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;3940const CARD_EXIT_MS = 120;41const CARD_ENTER_GAP_MS = 60;4243interface OnboardingProps {44 onComplete: () => void | Promise<void>;45 onRestore?: () => void | Promise<void>;46 onAdopt?: () => void | Promise<void>;47}4849export function Onboarding(props: OnboardingProps) {50 const [card, setCard] = createSignal(0);51 const [exiting, setExiting] = createSignal(false);52 const [completing, setCompleting] = createSignal(false);5354 async function advance() {55 if (exiting() || completing()) return;56 if (card() < CARDS.length - 1) {57 setExiting(true);58 window.setTimeout(() => {59 setCard((c) => c + 1);60 setExiting(false);61 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);62 } else {63 setCompleting(true);64 65 66 67 68 window.setTimeout(() => {69 void props.onComplete();70 }, 200);71 }72 }7374 function goBack() {75 if (exiting() || completing()) return;76 if (card() === 0) return;77 setExiting(true);78 window.setTimeout(() => {79 setCard((c) => c - 1);80 setExiting(false);81 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);82 }8384 return (85 <section86 class="peach-screen peach-theme peach-onboarding"87 classList={{ "is-completing": completing() }}88 >89 <RuneBackground opacity={0.07} />9091 <header class="peach-screen-header peach-onboarding-header">92 <PeachWordmark class="peach-wordmark-wrap" />93 </header>9495 <div96 class="peach-onboarding-card"97 classList={{98 "is-exiting": exiting(),99 "peach-onboarding-card--final": card() === CARDS.length - 1,100 }}101 onClick={() => {102 103 104 if (card() === CARDS.length - 1) return;105 advance();106 }}107 role="button"108 tabIndex={0}109 onKeyDown={(e) => {110 if (e.key === "Enter" || e.key === " ") {111 if (card() === CARDS.length - 1) return;112 e.preventDefault();113 advance();114 }115 }}116 >117 <h2 class="peach-onboarding-card-headline">118 {CARDS[card()]!.headline}119 </h2>120 <hr class="peach-onboarding-divider" />121 <For each={CARDS[card()]!.body}>122 {(para) => <p class="peach-onboarding-body">{para}</p>}123 </For>124 <Show when={CARDS[card()]!.warning}>125 <p class="peach-onboarding-warning">126 <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>127 <span>{CARDS[card()]!.warning}</span>128 </p>129 </Show>130 <Show when={card() === CARDS.length - 1}>131 <div class="peach-onboarding-choice-row">132 <button133 type="button"134 class="peach-onboarding-choice peach-onboarding-choice--create"135 onClick={(e) => {136 e.stopPropagation();137 void advance();138 }}139 >140 <span class="peach-onboarding-choice-label">Create a new vault</span>141 <span class="peach-onboarding-choice-detail">142 Set your master password and start saving.143 </span>144 </button>145 </div>146 <p class="peach-onboarding-choice-heading">Already using Peach?</p>147 <div class="peach-onboarding-choice-row">148 <button149 type="button"150 class="peach-onboarding-choice peach-onboarding-choice--adopt"151 onClick={(e) => {152 e.stopPropagation();153 void props.onAdopt?.();154 }}155 >156 <span class="peach-onboarding-choice-label">157 Pair with another device158 <span class="peach-pro-badge">Pro</span>159 </span>160 <span class="peach-onboarding-choice-detail">161 Scan a QR and confirm one 8-digit code.162 </span>163 </button>164 <button165 type="button"166 class="peach-onboarding-choice"167 onClick={(e) => {168 e.stopPropagation();169 void props.onRestore?.();170 }}171 >172 <span class="peach-onboarding-choice-label">Restore a backup</span>173 <span class="peach-onboarding-choice-detail">174 Use PeachScript, a .peach file, CSV, or Codex.175 </span>176 </button>177 </div>178 </Show>179 </div>180181 <div class="peach-onboarding-nav">182 <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>183 <button184 class="peach-onboarding-back"185 onClick={(e) => {186 e.stopPropagation();187 goBack();188 }}189 disabled={completing()}190 >191 ← back192 </button>193 </Show>194 <div class="peach-onboarding-dots" aria-hidden="true">195 <For each={CARDS}>196 {(_, i) => (197 <div198 class="peach-onboarding-dot"199 classList={{ "is-active": i() === card() }}200 />201 )}202 </For>203 </div>204 <button205 class="peach-onboarding-next"206 onClick={(e) => {207 e.stopPropagation();208 advance();209 }}210 disabled={completing()}211 >212 {card() === CARDS.length - 1 ? "Create vault" : "next →"}213 </button>214 </div>215 </section>216 );217}