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 cannot see it or reset it.",25 "Forget it, and the 12 recovery words you save at setup are the only way back in.",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 and passkeys.",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 return (76 <section77 class="peach-screen peach-theme peach-onboarding"78 classList={{ "is-completing": completing() }}79 >80 <RuneBackground opacity={0.07} />8182 <header class="peach-screen-header peach-onboarding-header">83 <PeachWordmark class="peach-wordmark-wrap" />84 </header>8586 <div87 class="peach-onboarding-card"88 classList={{ "is-exiting": exiting() }}89 onClick={advance}90 role="button"91 tabIndex={0}92 onKeyDown={(e) => {93 if (e.key === "Enter" || e.key === " ") {94 e.preventDefault();95 advance();96 }97 }}98 >99 <h2 class="peach-onboarding-card-headline">100 {CARDS[card()]!.headline}101 </h2>102 <hr class="peach-onboarding-divider" />103 <For each={CARDS[card()]!.body}>104 {(para) => <p class="peach-onboarding-body">{para}</p>}105 </For>106 <Show when={CARDS[card()]!.warning}>107 <p class="peach-onboarding-warning">108 <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>109 <span>{CARDS[card()]!.warning}</span>110 </p>111 </Show>112 </div>113114 <div class="peach-onboarding-nav">115 <div class="peach-onboarding-dots" aria-hidden="true">116 <For each={CARDS}>117 {(_, i) => (118 <div119 class="peach-onboarding-dot"120 classList={{ "is-active": i() === card() }}121 />122 )}123 </For>124 </div>125 <button126 class="peach-onboarding-next"127 onClick={(e) => {128 e.stopPropagation();129 advance();130 }}131 disabled={completing()}132 >133 {card() === CARDS.length - 1 ? "Get started →" : "next →"}134 </button>135 </div>136 </section>137 );138}