1import { For, Show, createSignal } from "solid-js";2import { PeachWordmark } from "../components/PeachMark";3import { RuneBackground } from "../components/RuneBackground";45interface OnboardingCard {6 headline: string;7 body: string[];8 warning: string | null;9}1011const BASE_CARDS: OnboardingCard[] = [12 {13 headline: "Your passwords\nlive here.\nNowhere else.",14 body: [15 "No account. No server ever accesses your vault. No one but you can see it.",16 "Everything is encrypted on your device with a password only you know.",17 ],18 warning: null,19 },20 {21 headline: "Two keys.",22 body: [23 "Your master password is yours alone. Peach never sees it.",24 "If you forget it, your 12 recovery words can reset it from settings — save them somewhere safe.",25 ],26 warning: "Lose both and your vault cannot be opened.",27 },28 {29 headline: "Everything\nyou need.",30 body: [31 "Free includes passwords, cards, secure notes, two-factor codes, autofill, strong passwords, and breach checks.",32 "Pro adds encrypted sync, Peach Codex paper restore, passkeys, advanced vault health, canary entries, and duress tools.",33 ],34 warning: null,35 },36];3738const CARDS: OnboardingCard[] = BASE_CARDS;3940414243export const ONBOARDING_LAST_CARD = CARDS.length - 1;4445const CARD_EXIT_MS = 120;46const CARD_ENTER_GAP_MS = 60;4748interface OnboardingProps {49 onComplete: () => void | Promise<void>;50 onRestore?: () => void | Promise<void>;51 onAdopt?: () => void | Promise<void>;52 535455 initialCard?: number;56}5758export function Onboarding(props: OnboardingProps) {59 const [card, setCard] = createSignal(props.initialCard ?? 0);60 const [exiting, setExiting] = createSignal(false);61 const [completing, setCompleting] = createSignal(false);6263 async function advance() {64 if (exiting() || completing()) return;65 if (card() < CARDS.length - 1) {66 setExiting(true);67 window.setTimeout(() => {68 setCard((c) => c + 1);69 setExiting(false);70 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);71 } else {72 setCompleting(true);73 74 75 76 77 window.setTimeout(() => {78 void props.onComplete();79 }, 200);80 }81 }8283 function goBack() {84 if (exiting() || completing()) return;85 if (card() === 0) return;86 setExiting(true);87 window.setTimeout(() => {88 setCard((c) => c - 1);89 setExiting(false);90 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);91 }9293 return (94 <section95 class="peach-screen peach-theme peach-onboarding"96 classList={{ "is-completing": completing() }}97 >98 <RuneBackground opacity={0.07} />99100 <header class="peach-screen-header peach-onboarding-header">101 <PeachWordmark class="peach-wordmark-wrap" />102 </header>103104 <div105 class="peach-onboarding-card"106 classList={{107 "is-exiting": exiting(),108 "peach-onboarding-card--final": card() === CARDS.length - 1,109 }}110 onClick={() => {111 112 113 if (card() === CARDS.length - 1) return;114 advance();115 }}116 role="button"117 tabIndex={0}118 onKeyDown={(e) => {119 if (e.key === "Enter" || e.key === " ") {120 if (card() === CARDS.length - 1) return;121 e.preventDefault();122 advance();123 }124 }}125 >126 <h2 class="peach-onboarding-card-headline">127 {CARDS[card()]!.headline}128 </h2>129 <hr class="peach-onboarding-divider" />130 <For each={CARDS[card()]!.body}>131 {(para) => <p class="peach-onboarding-body">{para}</p>}132 </For>133 <Show when={CARDS[card()]!.warning}>134 <p class="peach-onboarding-warning">135 <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>136 <span>{CARDS[card()]!.warning}</span>137 </p>138 </Show>139 <Show when={card() === CARDS.length - 1}>140 <p class="peach-onboarding-choice-heading">Already using Peach?</p>141 <div class="peach-onboarding-choice-row">142 <button143 type="button"144 class="peach-onboarding-choice peach-onboarding-choice--adopt"145 onClick={(e) => {146 e.stopPropagation();147 void props.onAdopt?.();148 }}149 >150 <span class="peach-onboarding-choice-label">151 Pair with another device152 <span class="peach-pro-badge">Pro</span>153 </span>154 <span class="peach-onboarding-choice-detail">155 Scan a QR and confirm one 8-digit code.156 </span>157 </button>158 <button159 type="button"160 class="peach-onboarding-choice"161 onClick={(e) => {162 e.stopPropagation();163 void props.onRestore?.();164 }}165 >166 <span class="peach-onboarding-choice-label">Restore a backup</span>167 <span class="peach-onboarding-choice-detail">168 Use PeachScript, a .peach file, CSV, or Codex.169 </span>170 </button>171 </div>172 </Show>173 </div>174175 <div class="peach-onboarding-nav">176 <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>177 <button178 class="peach-onboarding-back"179 onClick={(e) => {180 e.stopPropagation();181 goBack();182 }}183 disabled={completing()}184 >185 ← back186 </button>187 </Show>188 <div class="peach-onboarding-dots" aria-hidden="true">189 <For each={CARDS}>190 {(_, i) => (191 <div192 class="peach-onboarding-dot"193 classList={{ "is-active": i() === card() }}194 />195 )}196 </For>197 </div>198 <button199 class="peach-onboarding-next"200 onClick={(e) => {201 e.stopPropagation();202 advance();203 }}204 disabled={completing()}205 >206 {card() === CARDS.length - 1 ? "Create vault" : "next →"}207 </button>208 </div>209 </section>210 );211}