This commit is contained in:
Théo LAGACHE
2026-06-25 11:43:36 +02:00
parent 39ae2b8a8d
commit 70a551f3e8
6 changed files with 0 additions and 3460 deletions
@@ -1,980 +0,0 @@
# Onboarding Channel Config + Style Refactor Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a two-phase provider-select → configure flow to the notifier and storage onboarding steps (fake data, stored in flowData), restyle all toggle cards/buttons across onboarding steps to use design-system tokens matching organization-combobox.tsx, and wire conditional step skipping when no agents are created.
**Architecture:** `step-notifier` and `step-storage` manage a local `phase` state (`"grid"` | `"configuring"`). In "configuring" phase they render a `<Form>` using the existing `NotificationChannelFormSchema`/`StorageChannelFormSchema` and `renderChannelForm(provider, form)`. On submit, the configured channel `{ id, provider, label, name, config }` is appended to local state; no server action is called. All toggle `<button>` elements across steps replace hardcoded `border-white/10` with design-system tokens (`border-border`, `border-primary/20`, `bg-primary/10`, `hover:bg-accent/50`). The `agent-create` step gets a dynamic `nextStep` function that skips `agent-waiting`, `project-create`, and `db-settings` when `flowData.agents` is empty.
**Tech Stack:** Next.js App Router, `@onboardjs/react` 1.0.0-rc.5, `react-hook-form` + zod via `useZodForm`/`Form` from `@/components/ui/form`, existing `renderChannelForm` from `@/features/channel/channels-helpers`, `NotificationChannelFormSchema`/`StorageChannelFormSchema` from `@/features/channel/channel-form.schema`, shadcn/radix UI, lucide-react.
---
## File Structure
| File | Action | Responsibility |
|------|--------|----------------|
| `src/features/onboarding/onboarding.types.ts` | Modify | Extend `OnboardingChannel` with `name` + `config` fields |
| `src/features/onboarding/steps/step-notifier.tsx` | Rewrite | Two-phase grid→config flow for notification providers |
| `src/features/onboarding/steps/step-storage.tsx` | Rewrite | Two-phase grid→config flow for storage providers |
| `src/features/onboarding/steps/step-sso-gate.tsx` | Modify | Restyle SSO provider buttons to org-combobox card style |
| `src/features/onboarding/steps/step-security.tsx` | Modify | Restyle passkey/2FA choice to org-combobox card style |
| `src/features/onboarding/steps/step-preferences.tsx` | Modify | Restyle theme toggle buttons to org-combobox card style |
| `src/features/onboarding/steps/step-project-create.tsx` | Modify | Replace `border-white/10` with `border-border` on DB toggles |
| `src/features/onboarding/onboarding-steps.tsx` | Modify | Change `agent-create.nextStep` to a conditional function |
---
### Task 1: Extend `OnboardingChannel` type
**Files:**
- Modify: `src/features/onboarding/onboarding.types.ts`
- [ ] **Step 1: Update `OnboardingChannel`**
Replace lines 3135 in `src/features/onboarding/onboarding.types.ts`:
```typescript
export type OnboardingChannel = {
id: string;
provider: string;
label: string;
name: string;
config: Record<string, unknown>;
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: only the existing `step-account-info.tsx:28` better-auth error — `step-notifier.tsx` and `step-storage.tsx` will error until rewritten in Tasks 2 and 3, which is expected. No errors in `onboarding.types.ts` itself.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/onboarding.types.ts
git commit -m "feat(onboarding): extend OnboardingChannel with name and config fields"
```
---
### Task 2: Rewrite `step-notifier.tsx` — two-phase config flow + style
**Files:**
- Rewrite: `src/features/onboarding/steps/step-notifier.tsx`
**Context:** This step manages a `phase` local state. In `"grid"` phase it shows the provider selection grid. When the user clicks a provider, `phase` switches to `"configuring"` with the selected provider ID. In `"configuring"` phase it renders a full form using `NotificationChannelFormSchema` and `renderChannelForm`. On valid submit the channel is added to a local `channels` list and phase returns to `"grid"`. Clicking Continue saves `channels` to `flowData.notifiers` and calls `next()`. Provider cards use org-combobox design tokens. No server action is called.
- [ ] **Step 1: Write the file**
```typescript
// src/features/onboarding/steps/step-notifier.tsx
"use client";
import { useState } from "react";
import { useOnboarding } from "@onboardjs/react";
import { ArrowLeft, Check, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
useZodForm,
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
} from "@/components/ui/form";
import { notificationProviders } from "@/features/channel/channels-notification-helper";
import { renderChannelForm } from "@/features/channel/channels-helpers";
import { NotificationChannelFormSchema } from "@/features/channel/channel-form.schema";
import { OnboardingChannel } from "@/features/onboarding/onboarding.types";
type Phase = { kind: "grid" } | { kind: "configuring"; provider: string };
export const StepNotifier = () => {
const { next, updateContext, state } = useOnboarding();
const [phase, setPhase] = useState<Phase>({ kind: "grid" });
const [channels, setChannels] = useState<OnboardingChannel[]>([]);
// @ts-expect-error — discriminated union schema, provider set on phase entry
const form = useZodForm({ schema: NotificationChannelFormSchema });
const startConfiguring = (provider: string) => {
// @ts-expect-error — discriminated union
form.reset({ provider, enabled: true, name: "", config: {} });
setPhase({ kind: "configuring", provider });
};
const removeChannel = (id: string) => {
setChannels((prev) => prev.filter((c) => c.id !== id));
};
const onContinue = async () => {
await updateContext({ flowData: { ...state?.context.flowData, notifiers: channels } });
await next();
};
if (phase.kind === "configuring") {
const providerDetails = notificationProviders.find((p) => p.value === phase.provider);
const Icon = providerDetails?.icon;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3 p-3 bg-secondary/30 rounded-lg border border-border">
{Icon && (
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center">
<Icon className="size-5" />
</div>
)}
<p className="flex-1 text-sm font-medium">
Configuring {providerDetails?.label}
</p>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setPhase({ kind: "grid" })}
>
<ArrowLeft className="size-4 mr-1" />
Back
</Button>
</div>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values: any) => {
const details = notificationProviders.find(
(p) => p.value === values.provider
);
setChannels((prev) => [
...prev,
{
id: crypto.randomUUID(),
provider: values.provider,
label: details?.label ?? values.provider,
name: values.name,
config: values.config as Record<string, unknown>,
},
]);
// @ts-expect-error — discriminated union
form.reset({ enabled: true });
setPhase({ kind: "grid" });
}}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Channel name *</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
placeholder={`e.g. ${providerDetails?.label ?? ""} alerts`}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="provider"
render={({ field }) => (
<input type="hidden" {...field} value={field.value || ""} />
)}
/>
{renderChannelForm(phase.provider, form)}
<Button type="submit">Add channel</Button>
</Form>
</div>
);
}
// Phase: grid
const configuredProviderIds = channels.map((c) => c.provider);
const availableProviders = notificationProviders.filter((p) => !p.preview);
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Connect a notifier</h1>
<p className="text-sm text-muted-foreground mt-1">
Optional get notified about backups, restores and health checks.
</p>
</div>
{channels.length > 0 && (
<div className="flex flex-col gap-1">
{channels.map((ch) => {
const details = notificationProviders.find((p) => p.value === ch.provider);
const Icon = details?.icon;
return (
<div
key={ch.id}
className="flex items-center gap-2 rounded-lg border border-primary/20 bg-primary/10 p-2 text-sm text-primary"
>
{Icon && (
<div className="size-7 rounded-md border bg-muted/50 flex items-center justify-center shrink-0">
<Icon className="size-4" />
</div>
)}
<span className="flex-1 truncate">
{ch.name}{" "}
<span className="opacity-60">({ch.label})</span>
</span>
<button
type="button"
onClick={() => removeChannel(ch.id)}
className="opacity-50 hover:opacity-100 transition-opacity"
>
<X className="size-4" />
</button>
</div>
);
})}
</div>
)}
<div className="grid grid-cols-2 gap-2">
{availableProviders.map((provider) => {
const Icon = provider.icon;
const isConfigured = configuredProviderIds.includes(provider.value);
return (
<button
key={provider.value}
type="button"
onClick={() => startConfiguring(provider.value)}
className={`flex items-center gap-2 rounded-lg border p-2 text-sm transition-colors ${
isConfigured
? "border-primary/20 bg-primary/10 text-primary"
: "border-border hover:bg-accent/50 hover:border-primary/20"
}`}
>
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
<Icon className="size-4" />
</div>
<span className="flex-1 text-left">{provider.label}</span>
{isConfigured && (
<div className="size-5 rounded-full bg-primary flex items-center justify-center ml-auto">
<Check className="size-3 text-primary-foreground" strokeWidth={3} />
</div>
)}
</button>
);
})}
</div>
<Button type="button" onClick={onContinue}>
Continue
</Button>
</div>
);
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `step-notifier.tsx`.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/steps/step-notifier.tsx
git commit -m "feat(onboarding): add two-phase config flow and org-combobox style to notifier step"
```
---
### Task 3: Rewrite `step-storage.tsx` — two-phase config flow + style
**Files:**
- Rewrite: `src/features/onboarding/steps/step-storage.tsx`
**Context:** Identical pattern to Task 2 but for storage providers. Use `StorageChannelFormSchema` and `storageProviders`. Filter out `p.value === "local"` from the grid (local storage has no config form). `renderChannelForm("s3", form)` etc. renders the correct sub-form from `channels-helpers.tsx`.
- [ ] **Step 1: Write the file**
```typescript
// src/features/onboarding/steps/step-storage.tsx
"use client";
import { useState } from "react";
import { useOnboarding } from "@onboardjs/react";
import { ArrowLeft, Check, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
useZodForm,
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
} from "@/components/ui/form";
import { storageProviders } from "@/features/channel/channels-storage-helper";
import { renderChannelForm } from "@/features/channel/channels-helpers";
import { StorageChannelFormSchema } from "@/features/channel/channel-form.schema";
import { OnboardingChannel } from "@/features/onboarding/onboarding.types";
type Phase = { kind: "grid" } | { kind: "configuring"; provider: string };
export const StepStorage = () => {
const { next, updateContext, state } = useOnboarding();
const [phase, setPhase] = useState<Phase>({ kind: "grid" });
const [channels, setChannels] = useState<OnboardingChannel[]>([]);
// @ts-expect-error — discriminated union schema, provider set on phase entry
const form = useZodForm({ schema: StorageChannelFormSchema });
const startConfiguring = (provider: string) => {
// @ts-expect-error — discriminated union
form.reset({ provider, enabled: true, name: "", config: {} });
setPhase({ kind: "configuring", provider });
};
const removeChannel = (id: string) => {
setChannels((prev) => prev.filter((c) => c.id !== id));
};
const onContinue = async () => {
await updateContext({ flowData: { ...state?.context.flowData, storages: channels } });
await next();
};
if (phase.kind === "configuring") {
const providerDetails = storageProviders.find((p) => p.value === phase.provider);
const Icon = providerDetails?.icon;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3 p-3 bg-secondary/30 rounded-lg border border-border">
{Icon && (
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center">
<Icon className="size-5" />
</div>
)}
<p className="flex-1 text-sm font-medium">
Configuring {providerDetails?.label}
</p>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setPhase({ kind: "grid" })}
>
<ArrowLeft className="size-4 mr-1" />
Back
</Button>
</div>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values: any) => {
const details = storageProviders.find(
(p) => p.value === values.provider
);
setChannels((prev) => [
...prev,
{
id: crypto.randomUUID(),
provider: values.provider,
label: details?.label ?? values.provider,
name: values.name,
config: values.config as Record<string, unknown>,
},
]);
// @ts-expect-error — discriminated union
form.reset({ enabled: true });
setPhase({ kind: "grid" });
}}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Channel name *</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
placeholder={`e.g. ${providerDetails?.label ?? ""} backup`}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="provider"
render={({ field }) => (
<input type="hidden" {...field} value={field.value || ""} />
)}
/>
{renderChannelForm(phase.provider, form)}
<Button type="submit">Add storage</Button>
</Form>
</div>
);
}
// Phase: grid — filter "local" (no config form, no credentials)
const availableProviders = storageProviders.filter(
(p) => !p.preview && p.value !== "local"
);
const configuredProviderIds = channels.map((c) => c.provider);
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Connect a storage</h1>
<p className="text-sm text-muted-foreground mt-1">
Optional choose where backups and files are stored.
</p>
</div>
{channels.length > 0 && (
<div className="flex flex-col gap-1">
{channels.map((ch) => {
const details = storageProviders.find((p) => p.value === ch.provider);
const Icon = details?.icon;
return (
<div
key={ch.id}
className="flex items-center gap-2 rounded-lg border border-primary/20 bg-primary/10 p-2 text-sm text-primary"
>
{Icon && (
<div className="size-7 rounded-md border bg-muted/50 flex items-center justify-center shrink-0">
<Icon className="size-4" />
</div>
)}
<span className="flex-1 truncate">
{ch.name}{" "}
<span className="opacity-60">({ch.label})</span>
</span>
<button
type="button"
onClick={() => removeChannel(ch.id)}
className="opacity-50 hover:opacity-100 transition-opacity"
>
<X className="size-4" />
</button>
</div>
);
})}
</div>
)}
<div className="grid grid-cols-2 gap-2">
{availableProviders.map((provider) => {
const Icon = provider.icon;
const isConfigured = configuredProviderIds.includes(provider.value);
return (
<button
key={provider.value}
type="button"
onClick={() => startConfiguring(provider.value)}
className={`flex items-center gap-2 rounded-lg border p-2 text-sm transition-colors ${
isConfigured
? "border-primary/20 bg-primary/10 text-primary"
: "border-border hover:bg-accent/50 hover:border-primary/20"
}`}
>
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
<Icon className="size-4" />
</div>
<span className="flex-1 text-left">{provider.label}</span>
{isConfigured && (
<div className="size-5 rounded-full bg-primary flex items-center justify-center ml-auto">
<Check className="size-3 text-primary-foreground" strokeWidth={3} />
</div>
)}
</button>
);
})}
</div>
<Button type="button" onClick={onContinue}>
Continue
</Button>
</div>
);
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `step-storage.tsx`.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/steps/step-storage.tsx
git commit -m "feat(onboarding): add two-phase config flow and org-combobox style to storage step"
```
---
### Task 4: Restyle `step-sso-gate.tsx`
**Files:**
- Modify: `src/features/onboarding/steps/step-sso-gate.tsx`
**Context:** SSO provider buttons currently use shadcn `Button variant="outline"`. Replace with org-combobox card style using a `Globe` icon as placeholder (mockSsoConfig providers have no icon). "Continue with email" stays as a shadcn `Button`.
- [ ] **Step 1: Rewrite the file**
```typescript
// src/features/onboarding/steps/step-sso-gate.tsx
"use client";
import { useOnboarding } from "@onboardjs/react";
import { Globe } from "lucide-react";
import { Button } from "@/components/ui/button";
import { mockSsoConfig } from "@/features/onboarding/onboarding.mock";
export const StepSsoGate = () => {
const { next, updateContext, state } = useOnboarding();
const chooseProvider = async (providerId: string) => {
await updateContext({ flowData: { ...state?.context.flowData, sso: { providerId } } });
await next();
};
const continueWithEmail = async () => {
await next();
};
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Welcome to Portabase</h1>
<p className="text-sm text-muted-foreground mt-1">
Sign in with your organisation provider, or continue with email.
</p>
</div>
<div className="flex flex-col gap-2">
{mockSsoConfig.providers.map((provider) => (
<button
key={provider.id}
type="button"
onClick={() => chooseProvider(provider.id)}
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
>
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
<Globe className="size-4 text-muted-foreground" />
</div>
<span>Continue with {provider.label}</span>
</button>
))}
</div>
{!mockSsoConfig.forced && (
<Button type="button" onClick={continueWithEmail}>
Continue with email
</Button>
)}
</div>
);
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `step-sso-gate.tsx`.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/steps/step-sso-gate.tsx
git commit -m "style(onboarding): restyle sso-gate provider buttons to org-combobox card style"
```
---
### Task 5: Restyle `step-security.tsx`
**Files:**
- Modify: `src/features/onboarding/steps/step-security.tsx`
**Context:** Single-button choice (passkey or two-factor). Replace `Button` with an org-combobox card. Add a relevant lucide icon (`KeyRound` for passkey, `ShieldCheck` for two-factor) inside the icon container.
- [ ] **Step 1: Rewrite the file**
```typescript
// src/features/onboarding/steps/step-security.tsx
"use client";
import { useOnboarding } from "@onboardjs/react";
import { KeyRound, ShieldCheck } from "lucide-react";
import { mockSsoConfig } from "@/features/onboarding/onboarding.mock";
export const StepSecurity = () => {
const { next, updateContext, state } = useOnboarding();
const choose = async (method: "passkey" | "two-factor") => {
await updateContext({ flowData: { ...state?.context.flowData, security: { method } } });
await next();
};
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Secure your account</h1>
<p className="text-sm text-muted-foreground mt-1">
{mockSsoConfig.passkeyEnabled
? "Set up a passkey for faster, safer sign-in."
: "Set up two-factor authentication to protect your account."}
</p>
</div>
{mockSsoConfig.passkeyEnabled ? (
<button
type="button"
onClick={() => choose("passkey")}
className="flex items-center gap-3 rounded-lg border border-border p-4 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full text-left"
>
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
<KeyRound className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-col gap-0.5">
<span className="font-medium">Set up passkey</span>
<span className="text-xs text-muted-foreground">Faster, safer sign-in</span>
</div>
</button>
) : (
<button
type="button"
onClick={() => choose("two-factor")}
className="flex items-center gap-3 rounded-lg border border-border p-4 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full text-left"
>
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
<ShieldCheck className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-col gap-0.5">
<span className="font-medium">Set up two-factor</span>
<span className="text-xs text-muted-foreground">Add an extra layer of security</span>
</div>
</button>
)}
</div>
);
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `step-security.tsx`.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/steps/step-security.tsx
git commit -m "style(onboarding): restyle security method choice to org-combobox card style"
```
---
### Task 6: Restyle `step-preferences.tsx`
**Files:**
- Modify: `src/features/onboarding/steps/step-preferences.tsx`
**Context:** Theme toggle currently uses shadcn `Button` with `variant="default"/"outline"` switching. Replace with org-combobox toggle cards: active state `bg-primary/10 text-primary border-primary/20`, inactive `border-border hover:bg-accent/50`, check badge on active. Sun/Moon icons in icon containers.
- [ ] **Step 1: Rewrite the file**
```typescript
// src/features/onboarding/steps/step-preferences.tsx
"use client";
import { useState } from "react";
import { useOnboarding } from "@onboardjs/react";
import { Check, Moon, Sun } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
const MAX_AVATAR_SIZE_BYTES = 2 * 1024 * 1024;
export const StepPreferences = () => {
const { next, updateContext, state } = useOnboarding();
const [theme, setTheme] = useState<"light" | "dark">("dark");
const [avatarDataUrl, setAvatarDataUrl] = useState<string | undefined>(undefined);
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("Please select an image file.");
return;
}
if (file.size > MAX_AVATAR_SIZE_BYTES) {
toast.error("Image is too large. Please select a file under 2MB.");
return;
}
const reader = new FileReader();
reader.onload = () => setAvatarDataUrl(reader.result as string);
reader.onerror = () => toast.error("Failed to read the selected image. Please try again.");
reader.readAsDataURL(file);
};
const onContinue = async () => {
await updateContext({ flowData: { ...state?.context.flowData, preferences: { theme, avatarDataUrl } } });
await next();
};
const themeOptions: { value: "light" | "dark"; label: string; Icon: typeof Sun }[] = [
{ value: "light", label: "Light", Icon: Sun },
{ value: "dark", label: "Dark", Icon: Moon },
];
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Make yourself at home</h1>
<p className="text-sm text-muted-foreground mt-1">Pick your theme and add a profile photo.</p>
</div>
<div className="flex items-center gap-4">
<Avatar className="size-12">
<AvatarImage src={avatarDataUrl} alt="" />
<AvatarFallback>?</AvatarFallback>
</Avatar>
<label className="text-sm underline cursor-pointer">
Upload image
<input type="file" accept="image/*" className="hidden" onChange={onFileChange} />
</label>
</div>
<div className="flex gap-2">
{themeOptions.map(({ value, label, Icon }) => {
const isActive = theme === value;
return (
<button
key={value}
type="button"
onClick={() => setTheme(value)}
className={`flex items-center gap-2 rounded-lg border p-3 text-sm transition-colors flex-1 ${
isActive
? "border-primary/20 bg-primary/10 text-primary"
: "border-border hover:bg-accent/50 hover:border-primary/20"
}`}
>
<div className="size-7 rounded-md border bg-muted/50 flex items-center justify-center shrink-0">
<Icon className="size-4" />
</div>
<span className="flex-1 text-left">{label}</span>
{isActive && (
<div className="size-5 rounded-full bg-primary flex items-center justify-center">
<Check className="size-3 text-primary-foreground" strokeWidth={3} />
</div>
)}
</button>
);
})}
</div>
<Button type="button" onClick={onContinue}>
Continue
</Button>
</div>
);
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `step-preferences.tsx`.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/steps/step-preferences.tsx
git commit -m "style(onboarding): restyle theme toggles in preferences step to org-combobox card style"
```
---
### Task 7: Restyle `step-project-create.tsx`
**Files:**
- Modify: `src/features/onboarding/steps/step-project-create.tsx`
**Context:** DB toggle buttons have hardcoded `border-white/10` for inactive state. Replace with design-system tokens. Add icon container and check badge for active state (use `Database` icon from lucide).
- [ ] **Step 1: Rewrite the file**
```typescript
// src/features/onboarding/steps/step-project-create.tsx
"use client";
import { useState } from "react";
import { useOnboarding } from "@onboardjs/react";
import { Check, Database } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { mockDatabases } from "@/features/onboarding/onboarding.mock";
export const StepProjectCreate = () => {
const { next, updateContext, state } = useOnboarding();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [databaseIds, setDatabaseIds] = useState<string[]>([]);
const toggleDb = (id: string) => {
setDatabaseIds((prev) => (prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id]));
};
const onContinue = async () => {
await updateContext({ flowData: { ...state?.context.flowData, project: { name, description, databaseIds } } });
await next();
};
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Create a project</h1>
<p className="text-sm text-muted-foreground mt-1">Optional group databases under a project.</p>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="project-name">Project name</Label>
<Input id="project-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="My project" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="project-description">Description</Label>
<Textarea id="project-description" value={description} onChange={(e) => setDescription(e.target.value)} />
</div>
<div className="flex flex-col gap-2">
<Label>Databases</Label>
<div className="flex flex-col gap-2">
{mockDatabases.map((db) => {
const isSelected = databaseIds.includes(db.id);
return (
<button
key={db.id}
type="button"
onClick={() => toggleDb(db.id)}
className={`flex items-center gap-3 rounded-lg border p-3 text-sm transition-colors text-left ${
isSelected
? "border-primary/20 bg-primary/10 text-primary"
: "border-border hover:bg-accent/50 hover:border-primary/20"
}`}
>
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
<Database className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-col gap-0.5 flex-1">
<span className="font-medium">{db.name}</span>
<span className="text-xs text-muted-foreground">{db.engine}</span>
</div>
{isSelected && (
<div className="size-5 rounded-full bg-primary flex items-center justify-center ml-auto">
<Check className="size-3 text-primary-foreground" strokeWidth={3} />
</div>
)}
</button>
);
})}
</div>
</div>
<Button type="button" onClick={onContinue} disabled={!name.trim()}>
Continue
</Button>
</div>
);
};
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `step-project-create.tsx`.
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/steps/step-project-create.tsx
git commit -m "style(onboarding): restyle DB toggle buttons in project-create step to org-combobox card style"
```
---
### Task 8: Update `onboarding-steps.tsx` — conditional agent-create nextStep
**Files:**
- Modify: `src/features/onboarding/onboarding-steps.tsx`
**Context:** Change `agent-create.nextStep` from the static string `"agent-waiting"` to a function that checks `ctx.flowData.agents`. If agents were added → `"agent-waiting"` (normal flow). If none → `"finish"` (skips agent-waiting, project-create, db-settings). The `ctx` argument is the OnboardJS context object; its `flowData` property is `{ [key: string]: any }`.
- [ ] **Step 1: Update the `agent-create` entry in `onboarding-steps.tsx`**
Replace lines 8287:
```typescript
{
id: "agent-create",
component: StepAgentCreate,
isSkippable: true,
skipToStep: undefined,
nextStep: (ctx) => {
const agents = ctx.flowData?.agents as unknown[] | undefined;
return agents && agents.length > 0 ? "agent-waiting" : "finish";
},
},
```
- [ ] **Step 2: Verify type-check**
Run: `pnpm exec tsc --noEmit`
Expected: no errors in `onboarding-steps.tsx`. If TypeScript complains about the function signature for `nextStep`, check the `OnboardingStep` type in `node_modules/@onboardjs/react/dist/**/*.d.ts` and cast `ctx` as needed (e.g. `(ctx: any) => ...`).
- [ ] **Step 3: Commit**
```bash
git add src/features/onboarding/onboarding-steps.tsx
git commit -m "feat(onboarding): skip agent-waiting/project-create/db-settings when no agents created"
```
---
### Task 9: Final verification
**Files:** None — read-only checks.
- [ ] **Step 1: Full type-check**
Run: `pnpm exec tsc --noEmit 2>&1 | grep "onboarding"`
Expected: only the pre-existing `step-account-info.tsx:28` better-auth error. No new errors in any onboarding file.
- [ ] **Step 2: Verify step-invite-members and step-agent-create have no hardcoded colors**
Run:
```bash
grep -n "white/10\|zinc-\|#[0-9a-f]\{3,6\}" \
src/features/onboarding/steps/step-invite-members.tsx \
src/features/onboarding/steps/step-agent-create.tsx
```
Expected: no output. If any hardcoded color is found, replace with the design-system equivalent (`border-white/10``border-border`).
- [ ] **Step 3: Verify the step graph conditional skip**
Run: `grep -A8 '"agent-create"' src/features/onboarding/onboarding-steps.tsx`
Expected: output shows `nextStep: (ctx) => {` function, not a plain string.
- [ ] **Step 4: Commit if Step 2 required fixes**
Only commit if `step-invite-members.tsx` or `step-agent-create.tsx` were modified:
```bash
git add src/features/onboarding/steps/step-invite-members.tsx \
src/features/onboarding/steps/step-agent-create.tsx
git commit -m "style(onboarding): remove hardcoded colors from invite-members and agent-create"
```
File diff suppressed because it is too large Load Diff
@@ -1,194 +0,0 @@
# Onboarding Theme Persistence & Progress Bar Fix — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fix two independent bugs in the onboarding flow — theme not persisted to DB on selection, and progress bar not reflecting the user's true position in the full 15-step flow.
**Architecture:** Two surgical edits in two separate files. Task 1 adds a single `authClient.updateUser({ theme })` call in `StepPreferences`. Task 2 replaces OnboardJS-relative progress values in `OnboardingStepper` with a manual calculation based on `STEP_ORDER`.
**Tech Stack:** Next.js (App Router), React, TypeScript, OnboardJS (`@onboardjs/react`), `next-themes`, `better-auth` (`authClient`)
## Global Constraints
- TypeScript — no `any` introduced, no type assertions added
- No new dependencies
- No API, schema, or DB migration changes
- Follow existing import alias `@/` for all internal imports
- Commits in English, conventional-commits format (`fix:`)
---
### Task 1: Persist theme to DB on selection in StepPreferences
**Files:**
- Modify: `src/features/onboarding/steps/step-preferences.tsx` (lines ~51-61)
**Interfaces:**
- Consumes: `authClient.updateUser` from `@/lib/auth/auth-client` (already imported in the file)
- Produces: nothing consumed by Task 2
- [ ] **Step 1: Locate the `selectTheme` function**
Open `src/features/onboarding/steps/step-preferences.tsx`. Find the `selectTheme` function (~line 51). It currently reads:
```ts
const selectTheme = async (theme: ThemeKey) => {
// Apply immediately to the UI
setTheme(theme);
//mettre à jour aussi en db!
await updateContext({
flowData: {
...state?.context.flowData,
preferences: { ...preferences, theme },
},
});
};
```
- [ ] **Step 2: Add `authClient.updateUser({ theme })` call**
Replace the `selectTheme` function with:
```ts
const selectTheme = async (theme: ThemeKey) => {
// Apply immediately to the UI
setTheme(theme);
// Persist to DB so it survives page reload
await authClient.updateUser({ theme });
await updateContext({
flowData: {
...state?.context.flowData,
preferences: { ...preferences, theme },
},
});
};
```
Note: `authClient` is already imported at the top of the file (`import { authClient } from "@/lib/auth/auth-client";`). No new import needed.
- [ ] **Step 3: Verify TypeScript compiles cleanly**
```bash
cd /path/to/portabase && pnpm tsc --noEmit 2>&1 | grep step-preferences
```
Expected: no output (no errors on that file).
- [ ] **Step 4: Manual smoke test**
1. Start the dev server: `pnpm dev`
2. Open the onboarding flow in the browser
3. Navigate to the **Preferences** step
4. Click a theme (e.g. **Light**)
5. Verify the UI switches immediately
6. Open the Network tab → confirm a `PATCH` (or `POST`) request to the auth user update endpoint was made with `theme: "light"` in the payload
7. Hard-reload the page → theme should persist
- [ ] **Step 5: Commit**
```bash
git add src/features/onboarding/steps/step-preferences.tsx
git commit -m "fix(onboarding): persist theme to db on selection in StepPreferences"
```
---
### Task 2: Fix progress bar to reflect full 15-step position
**Files:**
- Modify: `src/features/onboarding/onboarding-stepper.tsx`
**Interfaces:**
- Consumes: `STEP_ORDER` from `@/features/onboarding/constants/steps` (already exported, already used in `onboarding-shell.tsx`)
- Produces: nothing consumed by Task 1
- [ ] **Step 1: Open the current stepper**
Open `src/features/onboarding/onboarding-stepper.tsx`. It currently reads:
```tsx
"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">
<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>
);
};
```
- [ ] **Step 2: Replace with STEP_ORDER-based calculation**
Replace the entire file content with:
```tsx
"use client";
import { useOnboarding } from "@onboardjs/react";
import { Progress } from "@/components/ui/progress";
import { STEP_ORDER } from "@/features/onboarding/constants/steps";
export const OnboardingStepper = () => {
const { state } = useOnboarding();
if (!state) return null;
const currentId = String(state.currentStep?.id ?? "");
const currentIndex = Math.max(0, STEP_ORDER.indexOf(currentId));
const totalSteps = STEP_ORDER.length;
const stepNumber = currentIndex + 1;
const progress = Math.round((currentIndex / (totalSteps - 1)) * 100);
return (
<div className="flex flex-col gap-2 w-full">
<div className="flex justify-between text-xs text-muted-foreground">
<span>
Step {stepNumber} of {totalSteps}
</span>
<span>{progress}%</span>
</div>
<Progress value={progress} />
</div>
);
};
```
**Why `Math.max(0, ...)`:** If `currentId` is not in `STEP_ORDER` (unknown step), `indexOf` returns `-1`. Clamping to `0` gives a safe fallback of "Step 1 of 15 — 0%" rather than a negative/NaN value.
**Why `totalSteps - 1` in the divisor:** At `login` (index 0) → 0%. At `finish` (index 14) → 14/14 = 100%. Without the `-1` the last step would be 93%.
- [ ] **Step 3: Verify TypeScript compiles cleanly**
```bash
pnpm tsc --noEmit 2>&1 | grep onboarding-stepper
```
Expected: no output.
- [ ] **Step 4: Manual smoke test**
1. Simulate resuming onboarding mid-flow (e.g. log out, log back in as a user who already has an org — should resume at `invite-members`, index 5)
2. Verify the stepper shows **"Step 6 of 15 — 36%"** and the progress bar is ~1/3 filled
3. Click through a few steps and verify the number and percentage increase correctly each time
4. Reach the `finish` step and verify **"Step 15 of 15 — 100%"**
- [ ] **Step 5: Commit**
```bash
git add src/features/onboarding/onboarding-stepper.tsx
git commit -m "fix(onboarding): recalculate progress bar from full STEP_ORDER position"
```
@@ -1,172 +0,0 @@
# Onboarding Channel Config + Style Refactor
**Date:** 2026-06-17
**Branch:** poc/onboarding-setup
**Scope:** Two concerns:
1. Notifier/storage steps gain a real two-phase configuration flow (fake data, stored in flowData)
2. All onboarding step cards, toggles, and inputs adopt the org-combobox design language
---
## 1. Configuration Flow — Notifier & Storage Steps
### Goal
When a user clicks a provider in the notifier or storage step, they should be able to configure it (Slack webhook URL, S3 credentials, etc.) before it is added to their list. Data is stored in `flowData` only — no server actions are called during onboarding.
### Phase Machine
Each step manages a local phase:
```ts
type Phase =
| { kind: "grid" }
| { kind: "configuring"; provider: string }
```
**Phase "grid"**
- Shows the provider selection grid (style: see Section 2)
- Already-configured providers show at top with check badge and remove button
- Unconfigured providers are clickable → transition to `configuring`
- "Continue" button visible if at least 0 channels configured (step is optional)
- "Add another" link/button visible when at least 1 configured
**Phase "configuring"**
- Header: provider icon + name + "Back" button (returns to grid, no save)
- Channel name `Input` (required, min 5 chars per existing schema)
- `renderChannelForm(provider, form)` renders the provider-specific fields
- `useZodForm` with `NotificationChannelFormSchema` (notifier) or `StorageChannelFormSchema` (storage)
- Submit validates → appends `{ id: crypto.randomUUID(), provider, label, name, config }` to local channels list → returns to grid phase
- No server action called
### Updated `OnboardingChannel` type
```ts
export type OnboardingChannel = {
id: string;
provider: string;
label: string;
name: string;
config: Record<string, unknown>;
};
```
`step-defaults.tsx` reads `id` and `label` from notifiers/storages — unchanged, continues to work.
### Files
- `src/features/onboarding/onboarding.types.ts` — extend `OnboardingChannel`
- `src/features/onboarding/steps/step-notifier.tsx` — full rewrite
- `src/features/onboarding/steps/step-storage.tsx` — full rewrite
### Dependencies reused
- `renderChannelForm` from `@/features/channel/channels-helpers`
- `NotificationChannelFormSchema` / `StorageChannelFormSchema` from `@/features/channel/channel-form.schema`
- `useZodForm`, `Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormMessage` from `@/components/ui/form`
- `notificationProviders` / `storageProviders` from their respective helpers
---
## 2. Style Refactor — All Onboarding Steps
### Goal
Replace hardcoded dark colors and inconsistent `<button>` styling across all step components with design-system tokens matching the org-combobox visual language.
### Design Tokens
**Toggle/selection cards** (provider cards, theme buttons, DB toggles, SSO buttons, security method buttons):
| State | Classes |
|----------|---------|
| Inactive | `border border-border rounded-lg hover:bg-accent/50 hover:border-primary/20 transition-colors` |
| Active | `border border-primary/20 bg-primary/10 text-primary rounded-lg` |
**Icon container** (inside provider cards):
```
size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center
```
**Check badge** (selected state on provider cards):
```
size-5 rounded-full bg-primary flex items-center justify-center ml-auto
→ Check className="size-3 text-primary-foreground" strokeWidth={3}
```
**Inputs** (`Input`, `Textarea`): already shadcn — remove any hardcoded color overrides. No component change.
**Navigation buttons** (`Button` shadcn variants `default`/`outline`/`ghost`): unchanged, already correct.
**Shell** (`onboarding-shell.tsx`): `bg-zinc-950`/`bg-zinc-900` intentionally kept dark — not changed.
### Files Affected
| File | Change |
|------|--------|
| `step-sso-gate.tsx` | SSO provider buttons → toggle card style |
| `step-security.tsx` | Passkey/2FA method buttons → toggle card style |
| `step-preferences.tsx` | Theme toggle buttons → toggle card style |
| `step-project-create.tsx` | DB toggle buttons → toggle card style |
| `step-notifier.tsx` | Provider cards → toggle card style (done as part of config flow rewrite) |
| `step-storage.tsx` | Provider cards → toggle card style (done as part of config flow rewrite) |
| `step-invite-members.tsx` | Audit for hardcoded colors, clean if needed |
| `step-agent-create.tsx` | Audit for hardcoded colors, clean if needed |
---
## 3. Conditional Step Graph — Skip Agent Steps When No Agents
### Rule
If the user skips or submits `agent-create` with zero agents (`flowData.agents` empty or undefined), skip `agent-waiting`, `project-create`, and `db-settings` — jump directly to `finish`.
### Step Graph Change
`onboarding-steps.tsx``agent-create` entry:
```ts
{
id: "agent-create",
component: StepAgentCreate,
isSkippable: true,
nextStep: (ctx) => {
const agents = ctx.flowData?.agents as unknown[] | undefined;
return agents && agents.length > 0 ? "agent-waiting" : "finish";
},
}
```
All other steps unchanged.
### Rationale
- `agent-waiting` pings the agent — no agent to ping
- `project-create` groups databases under an agent project — no agent, no project
- `db-settings` configures agent DB retention — no project selected, empty state anyway
### Files
- `src/features/onboarding/onboarding-steps.tsx` — update `agent-create.nextStep`
---
## Out of Scope
- Server actions / real DB persistence during onboarding (fake data only)
- `step-account-info.tsx`, `step-org-create.tsx`, `step-defaults.tsx`, `step-agent-waiting.tsx`, `step-finish.tsx` — no toggle cards, no hardcoded colors
- `onboarding-shell.tsx` background colors — intentionally dark
- Any changes outside `src/features/onboarding/`
---
## Acceptance Criteria
1. Clicking a provider in notifier/storage step → shows config form with correct provider-specific fields
2. Submit config form → validates with existing schema → provider appears in grid with check badge and remove button
3. Continue → saves all configured channels (with `name` + `config`) to `flowData.notifiers` / `flowData.storages`
4. All toggle cards across steps use design-system tokens (no `border-white/10`, no hardcoded bg)
5. `step-defaults.tsx` selects still work (reads `id` + `label` from channels)
6. `pnpm exec tsc --noEmit` — no new errors in onboarding files
7. Skip agent-create with 0 agents → jumps to finish (agent-waiting, project-create, db-settings not shown)
8. Create agents → normal flow through agent-waiting → project-create → db-settings → finish
@@ -1,111 +0,0 @@
# Onboarding Feature Refactor — Design Spec
_2026-06-19_
## Goal
Reorganise `src/features/onboarding/` into clean sub-directories. Replace ad-hoc `useState(loading)` + try/finally patterns with `useMutation`/`useQuery` hooks. Steps become pure JSX shells.
## Directory Structure (target)
```
onboarding/
actions/ ← server actions (unchanged)
constants/
steps.ts ← STEP_ORDER array + STEP_IDS const
hooks/
use-create-agent.ts
use-delete-agent.ts
use-agent-status.ts ← useQuery, 3s poll
use-create-org.ts
use-create-project.ts
use-update-account.ts
use-add-notifier.ts
use-remove-notifier.ts
use-add-storage.ts
use-remove-storage.ts
use-generate-edge-key.ts
use-mark-onboarding-done.ts
schemas/
account.schema.ts ← BaseSchema + WithPasswordSchema
types/
index.ts ← content of onboarding.types.ts (moved)
utils/ ← reserved, empty for now
steps/ ← existing components, thinned to JSX
onboarding-checklist.tsx
onboarding-shell.tsx
onboarding-state.ts
onboarding-stepper.tsx
onboarding-steps.tsx
is-onboarding-done.ts
onboarding.types.ts ← deleted once types/index.ts takes over
```
## Hook Contract
Every hook calls `useOnboarding()` internally. Steps pass nothing — they only import the hook.
**Mutation hooks** return `UseMutationResult` directly so steps get `mutate`, `isPending`, `isError` etc. from TanStack Query natively.
```ts
// Pattern
export const useCreateAgent = () => {
const { state, updateContext } = useOnboarding();
return useMutation({
mutationFn: async (name: string) => {
const orgId = (state?.context.flowData.org as any)?.id;
if (!orgId) throw new Error("Missing org ID");
const result = await createAgentAction({ organizationId: orgId, data: { name, description: "" } });
if (!result?.data?.data) throw new Error(result?.serverError ?? "Failed to create agent");
const newAgent: OnboardingAgent = { id: result.data.data.id, name: result.data.data.name };
const agents = [...((state?.context.flowData.agents ?? []) as OnboardingAgent[]), newAgent];
await updateContext({ flowData: { ...state?.context.flowData, agents } });
return newAgent;
},
onError: (err: Error) => toast.error(err.message),
});
};
```
**Query hooks** return `UseQueryResult`:
```ts
export const useAgentStatus = () => {
const { state } = useOnboarding();
const agentId = (state?.context.flowData.agents as OnboardingAgent[])?.[0]?.id;
return useQuery({
queryKey: ["onboarding-agent-status", agentId],
queryFn: async () => { /* getAgentStatusAction */ },
refetchInterval: 3_000,
enabled: !!agentId,
});
};
```
## Constants
`constants/steps.ts` exports:
- `STEP_ORDER: string[]` — the full ordered list used by the shell for Back/Next logic
- `STEP_IDS` — typed const object `{ LOGIN: "login", ... }` for safe step references
## Schemas
`schemas/account.schema.ts` exports `BaseSchema` and `WithPasswordSchema` (currently inline in `step-account-info.tsx`).
## Types
`types/index.ts` is a verbatim move of `onboarding.types.ts`. All existing imports updated from `@/features/onboarding/onboarding.types``@/features/onboarding/types`.
## Steps After Refactor
Each step file shrinks to: imports + JSX + hook call. No direct action imports, no manual loading state.
Example diff for `step-agent-create.tsx`:
- Remove: `createAgentAction`, `deleteAgentAction`, `useState(adding)`, manual try/finally
- Add: `useCreateAgent()`, `useDeleteAgent()`
## Out of Scope
- `onboarding-state.ts` server logic — untouched
- `onboarding-steps.tsx` step registry — untouched
- UI/UX changes to any step
- Adding new steps
@@ -1,98 +0,0 @@
# Onboarding — Theme Persistence & Progress Bar Fix
**Date:** 2026-06-19
**Scope:** `src/features/onboarding`
**Status:** Approved
---
## Problem Statement
Two independent bugs in the onboarding flow:
1. **Theme not persisted to DB** — selecting a theme in `StepPreferences` applies it visually via `next-themes` but never calls `authClient.updateUser({ theme })`, so the preference is lost on page reload unless the user also saves an avatar.
2. **Progress bar starts from current step, not from the beginning**`OnboardingStepper` uses `state.progressPercentage`, `state.currentStepNumber`, and `state.totalSteps` from OnboardJS. When onboarding resumes mid-flow (e.g. at step `invite-members`), OnboardJS recalculates these values relative to the remaining steps in the current session, not relative to the full 15-step flow. This makes the bar start at 0% even though the user is already 40%+ through.
---
## Fix 1 — Theme Persistence
**File:** `src/features/onboarding/steps/step-preferences.tsx`
### Current behaviour
```ts
const selectTheme = async (theme: ThemeKey) => {
setTheme(theme); // ✅ updates next-themes (UI)
// mettre à jour aussi en db! ← comment acknowledging the bug
await updateContext({ ... });
};
```
### Target behaviour
```ts
const selectTheme = async (theme: ThemeKey) => {
setTheme(theme);
await authClient.updateUser({ theme }); // ← add this line
await updateContext({ ... });
};
```
**Reference:** `src/features/profile/profile-appearance.tsx` already does this correctly.
**Error handling:** No change — if `updateUser` fails, the UI theme is still applied and the user can continue. No toast/error needed at this stage.
---
## Fix 2 — Progress Bar Recalculation
**File:** `src/features/onboarding/onboarding-stepper.tsx`
### Root cause
OnboardJS computes `progressPercentage` and `currentStepNumber` relative to the steps it was initialised with in the current session. When resuming mid-flow, the step list seen by OnboardJS starts at the resume step, not at `login`.
### Solution
Replace all OnboardJS progress values with a manual calculation based on `STEP_ORDER` (the canonical ordered list of all 15 steps, already defined in `src/features/onboarding/constants/steps.ts`).
```ts
import { STEP_ORDER } from "@/features/onboarding/constants/steps";
const currentId = String(state?.currentStep?.id ?? "");
const currentIndex = Math.max(0, STEP_ORDER.indexOf(currentId)); // guard: -1 → 0
const totalSteps = STEP_ORDER.length; // 15
const stepNumber = currentIndex + 1; // 1-based
const progress = Math.round((currentIndex / (totalSteps - 1)) * 100);
```
| Step | Index | stepNumber | progress |
|------------------|-------|------------|----------|
| login | 0 | 1 of 15 | 0% |
| preferences | 3 | 4 of 15 | 21% |
| invite-members | 5 | 6 of 15 | 36% |
| storage | 7 | 8 of 15 | 50% |
| finish | 14 | 15 of 15 | 100% |
**Edge case:** If `currentId` is not found in `STEP_ORDER`, `indexOf` returns `-1`, clamped to `0` by `Math.max` → shows step 1 of 15 at 0%. Safe degradation.
---
## Files Changed
| File | Change |
|------|--------|
| `src/features/onboarding/steps/step-preferences.tsx` | Add `authClient.updateUser({ theme })` in `selectTheme` |
| `src/features/onboarding/onboarding-stepper.tsx` | Replace OnboardJS progress values with `STEP_ORDER`-based calculation |
No new files, no schema changes, no API changes.
---
## Out of Scope
- Adding a toast/error on failed theme save
- Persisting the avatar selection to DB during selection (only on "Continue" — existing behaviour unchanged)
- Refactoring OnboardJS step initialisation