1import { For, Show, createSignal } from "solid-js";2import { storageSet } from "../../shared/storage";3import { PeachWordmark } from "../components/PeachMark";4import { RuneBackground } from "../components/RuneBackground";56interface OnboardingCard {7 headline: string;8 body: string[];9 warning: string | null;10}1112const BASE_CARDS: OnboardingCard[] = [13 {14 headline: "Welcome to\nPeach Passwords!",15 body: [16 "Your passwords live here. Nowhere else.",17 "No account. No server ever accesses your vault. No one but you can see it.",18 "Everything is encrypted on your device with a password only you know.",19 ],20 warning: null,21 },22 {23 headline: "Two keys.",24 body: [25 "Your master password is yours alone. Peach never sees it.",26 "If you forget it, your 12 recovery words can reset it from settings — save them somewhere safe.",27 ],28 warning: "Lose both and your vault cannot be opened.",29 },30 {31 headline: "Everything\nyou need.",32 body: [33 "Free includes passwords, cards, secure notes, two-factor codes, autofill, strong passwords, and breach checks.",34 "Pro adds encrypted sync, Peach Codex paper restore, passkeys, advanced vault health, canary entries, and duress tools.",35 ],36 warning: null,37 },38];3940const CARDS: OnboardingCard[] = BASE_CARDS;4142434445export const ONBOARDING_LAST_CARD = CARDS.length - 1;46474849export const ONBOARDING_CARD_STORAGE_KEY = "peach_onboarding_card";5051const CARD_EXIT_MS = 120;52const CARD_ENTER_GAP_MS = 60;5354interface OnboardingProps {55 onComplete: () => void | Promise<void>;56 onRestore?: () => void | Promise<void>;57 onAdopt?: () => void | Promise<void>;58 596061 initialCard?: number;62}6364export function Onboarding(props: OnboardingProps) {65 const [card, setCard] = createSignal(props.initialCard ?? 0);66 const [exiting, setExiting] = createSignal(false);67 const [completing, setCompleting] = createSignal(false);6869 70 71 function persistCard(index: number) {72 void storageSet(ONBOARDING_CARD_STORAGE_KEY, String(index)).catch(() => {});73 }7475 async function advance() {76 if (exiting() || completing()) return;77 if (card() < CARDS.length - 1) {78 setExiting(true);79 window.setTimeout(() => {80 setCard((c) => c + 1);81 persistCard(card());82 setExiting(false);83 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);84 } else {85 setCompleting(true);86 87 88 89 90 window.setTimeout(() => {91 void props.onComplete();92 }, 200);93 }94 }9596 function goBack() {97 if (exiting() || completing()) return;98 if (card() === 0) return;99 setExiting(true);100 window.setTimeout(() => {101 setCard((c) => c - 1);102 persistCard(card());103 setExiting(false);104 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);105 }106107 return (108 <section109 class="peach-screen peach-theme peach-onboarding"110 classList={{ "is-completing": completing() }}111 >112 <RuneBackground opacity={0.07} />113114 <header class="peach-screen-header peach-onboarding-header">115 <PeachWordmark class="peach-wordmark-wrap" />116 </header>117118 <div119 class="peach-onboarding-card"120 classList={{121 "is-exiting": exiting(),122 "peach-onboarding-card--final": card() === CARDS.length - 1,123 }}124 onClick={() => {125 126 127 if (card() === CARDS.length - 1) return;128 advance();129 }}130 role="button"131 tabIndex={0}132 onKeyDown={(e) => {133 if (e.key === "Enter" || e.key === " ") {134 if (card() === CARDS.length - 1) return;135 e.preventDefault();136 advance();137 }138 }}139 >140 <h2 class="peach-onboarding-card-headline">141 {CARDS[card()]!.headline}142 </h2>143 <hr class="peach-onboarding-divider" />144 <For each={CARDS[card()]!.body}>145 {(para) => <p class="peach-onboarding-body">{para}</p>}146 </For>147 <Show when={CARDS[card()]!.warning}>148 <p class="peach-onboarding-warning">149 <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>150 <span>{CARDS[card()]!.warning}</span>151 </p>152 </Show>153 <Show when={card() === CARDS.length - 1}>154 <p class="peach-onboarding-choice-heading">Already using Peach?</p>155 <div class="peach-onboarding-choice-row">156 <button157 type="button"158 class="peach-onboarding-choice peach-onboarding-choice--adopt"159 onClick={(e) => {160 e.stopPropagation();161 void props.onAdopt?.();162 }}163 >164 <span class="peach-onboarding-choice-label">165 Pair with another device166 <span class="peach-pro-badge">Pro</span>167 </span>168 <span class="peach-onboarding-choice-detail">169 Scan a QR and confirm one 8-digit code.170 </span>171 </button>172 <button173 type="button"174 class="peach-onboarding-choice"175 onClick={(e) => {176 e.stopPropagation();177 void props.onRestore?.();178 }}179 >180 <span class="peach-onboarding-choice-label">Restore a backup</span>181 <span class="peach-onboarding-choice-detail">182 Use PeachScript, a .peach file, CSV, or Codex.183 </span>184 </button>185 </div>186 </Show>187 </div>188189 <div class="peach-onboarding-nav">190 <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>191 <button192 class="peach-onboarding-back"193 onClick={(e) => {194 e.stopPropagation();195 goBack();196 }}197 disabled={completing()}198 >199 ← back200 </button>201 </Show>202 <div class="peach-onboarding-dots" aria-hidden="true">203 <For each={CARDS}>204 {(_, i) => (205 <div206 class="peach-onboarding-dot"207 classList={{ "is-active": i() === card() }}208 />209 )}210 </For>211 </div>212 <button213 class="peach-onboarding-next"214 onClick={(e) => {215 e.stopPropagation();216 advance();217 }}218 disabled={completing()}219 >220 {card() === CARDS.length - 1 ? "Create vault" : "next →"}221 </button>222 </div>223 </section>224 );225}