1import { For, Show, createSignal } from "solid-js";2import { PeachWordmark } from "../components/PeachMark";3import { RuneBackground } from "../components/RuneBackground";4import { storageSet } from "../../shared/storage";56interface OnboardingCard {7 headline: string;8 body: string[];9 warning: string | null;10}1112const BASE_CARDS: OnboardingCard[] = [13 {14 headline: "Your passwords\nlive here.\nNowhere else.",15 body: [16 "No account. No server. No one but you can ever see your vault.",17 "Everything is encrypted on your device with a password only you know.",18 ],19 warning: null,20 },21 {22 headline: "Two keys.",23 body: [24 "Your master password is yours alone. Peach never sees it.",25 "If you forget it, your 12 recovery words can reset it from settings — save them somewhere safe.",26 ],27 warning: "Lose both and your vault cannot be opened.",28 },29 {30 headline: "Everything\nyou need.",31 body: [32 "Passwords, passkeys, cards, secure notes, two-factor codes.",33 "Autofills on every site. Generates strong passwords. Detects breaches.",34 "Free forever. Pro adds sync, passkeys, and advanced vault health.",35 ],36 warning: null,37 },38];3940const CARDS: OnboardingCard[] = BASE_CARDS;4142const CARD_EXIT_MS = 120;43const CARD_ENTER_GAP_MS = 60;4445interface OnboardingProps {46 onComplete: () => 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 try {65 await storageSet("peach_onboarded", "true");66 } catch {67 68 }69 window.setTimeout(() => {70 void props.onComplete();71 }, 200);72 }73 }7475 function goBack() {76 if (exiting() || completing()) return;77 if (card() === 0) return;78 setExiting(true);79 window.setTimeout(() => {80 setCard((c) => c - 1);81 setExiting(false);82 }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);83 }8485 return (86 <section87 class="peach-screen peach-theme peach-onboarding"88 classList={{ "is-completing": completing() }}89 >90 <RuneBackground opacity={0.07} />9192 <header class="peach-screen-header peach-onboarding-header">93 <PeachWordmark class="peach-wordmark-wrap" />94 </header>9596 <div97 class="peach-onboarding-card"98 classList={{ "is-exiting": exiting() }}99 onClick={advance}100 role="button"101 tabIndex={0}102 onKeyDown={(e) => {103 if (e.key === "Enter" || e.key === " ") {104 e.preventDefault();105 advance();106 }107 }}108 >109 <h2 class="peach-onboarding-card-headline">110 {CARDS[card()]!.headline}111 </h2>112 <hr class="peach-onboarding-divider" />113 <For each={CARDS[card()]!.body}>114 {(para) => <p class="peach-onboarding-body">{para}</p>}115 </For>116 <Show when={CARDS[card()]!.warning}>117 <p class="peach-onboarding-warning">118 <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>119 <span>{CARDS[card()]!.warning}</span>120 </p>121 </Show>122 </div>123124 <div class="peach-onboarding-nav">125 <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>126 <button127 class="peach-onboarding-back"128 onClick={(e) => {129 e.stopPropagation();130 goBack();131 }}132 disabled={completing()}133 >134 ← back135 </button>136 </Show>137 <div class="peach-onboarding-dots" aria-hidden="true">138 <For each={CARDS}>139 {(_, i) => (140 <div141 class="peach-onboarding-dot"142 classList={{ "is-active": i() === card() }}143 />144 )}145 </For>146 </div>147 <button148 class="peach-onboarding-next"149 onClick={(e) => {150 e.stopPropagation();151 advance();152 }}153 disabled={completing()}154 >155 {card() === CARDS.length - 1 ? "Get started →" : "next →"}156 </button>157 </div>158 </section>159 );160}