# 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 `
` 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 ` { 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, }, ]); // @ts-expect-error — discriminated union form.reset({ enabled: true }); setPhase({ kind: "grid" }); }} > ( Channel name * )} /> ( )} /> {renderChannelForm(phase.provider, form)} ); } // Phase: grid const configuredProviderIds = channels.map((c) => c.provider); const availableProviders = notificationProviders.filter((p) => !p.preview); return (

Connect a notifier

Optional — get notified about backups, restores and health checks.

{channels.length > 0 && (
{channels.map((ch) => { const details = notificationProviders.find((p) => p.value === ch.provider); const Icon = details?.icon; return (
{Icon && (
)} {ch.name}{" "} ({ch.label})
); })}
)}
{availableProviders.map((provider) => { const Icon = provider.icon; const isConfigured = configuredProviderIds.includes(provider.value); return ( ); })}
); }; ``` - [ ] **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({ kind: "grid" }); const [channels, setChannels] = useState([]); // @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 (
{Icon && (
)}

Configuring {providerDetails?.label}

{ 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, }, ]); // @ts-expect-error — discriminated union form.reset({ enabled: true }); setPhase({ kind: "grid" }); }} > ( Channel name * )} /> ( )} /> {renderChannelForm(phase.provider, form)}
); } // 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 (

Connect a storage

Optional — choose where backups and files are stored.

{channels.length > 0 && (
{channels.map((ch) => { const details = storageProviders.find((p) => p.value === ch.provider); const Icon = details?.icon; return (
{Icon && (
)} {ch.name}{" "} ({ch.label})
); })}
)}
{availableProviders.map((provider) => { const Icon = provider.icon; const isConfigured = configuredProviderIds.includes(provider.value); return ( ); })}
); }; ``` - [ ] **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 (

Welcome to Portabase

Sign in with your organisation provider, or continue with email.

{mockSsoConfig.providers.map((provider) => ( ))}
{!mockSsoConfig.forced && ( )}
); }; ``` - [ ] **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 (

Secure your account

{mockSsoConfig.passkeyEnabled ? "Set up a passkey for faster, safer sign-in." : "Set up two-factor authentication to protect your account."}

{mockSsoConfig.passkeyEnabled ? ( ) : ( )}
); }; ``` - [ ] **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(undefined); const onFileChange = (e: React.ChangeEvent) => { 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 (

Make yourself at home

Pick your theme and add a profile photo.

?
{themeOptions.map(({ value, label, Icon }) => { const isActive = theme === value; return ( ); })}
); }; ``` - [ ] **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([]); 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 (

Create a project

Optional — group databases under a project.

setName(e.target.value)} placeholder="My project" />