Onboarding.tsx

3.8 KB · raw

1import { For, Show, createSignal } from "solid-js";23import { PeachWordmark } from "../components/PeachMark";4import { RuneBackground } from "../components/RuneBackground";56interface OnboardingCard {7  headline: string;8  body: string[];9  warning: string | null;10}1112const 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: "One rule.",23    body: [24      "Your master password is yours alone. Peach cannot recover it. There is no reset.",25      "Choose something you will remember. Write it down somewhere safe.",26    ],27    warning: "If you forget it, 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, mobile, and passkeys.",35    ],36    warning: null,37  },38];3940const CARD_EXIT_MS = 120;41const CARD_ENTER_GAP_MS = 60;4243interface OnboardingProps {44  onComplete: () => void | Promise<void>;45}4647export function Onboarding(props: OnboardingProps) {48  const [card, setCard] = createSignal(0);49  const [exiting, setExiting] = createSignal(false);50  const [completing, setCompleting] = createSignal(false);5152  function advance() {53    if (exiting() || completing()) return;54    if (card() < CARDS.length - 1) {55      setExiting(true);56      window.setTimeout(() => {57        setCard((c) => c + 1);58        setExiting(false);59      }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);60    } else {61      setCompleting(true);62      try {63        chrome.storage.local.set({ peach_onboarded: true });64      } catch {65        // storage unavailable — still complete the flow66      }67      window.setTimeout(() => {68        void props.onComplete();69      }, 200);70    }71  }7273  return (74    <section75      class="peach-screen peach-theme peach-onboarding"76      classList={{ "is-completing": completing() }}77    >78      <RuneBackground opacity={0.028} />7980      <header class="peach-screen-header peach-onboarding-header">81        <PeachWordmark class="peach-wordmark-wrap" />82      </header>8384      <div85        class="peach-onboarding-card"86        classList={{ "is-exiting": exiting() }}87        onClick={advance}88        role="button"89        tabIndex={0}90        onKeyDown={(e) => {91          if (e.key === "Enter" || e.key === " ") {92            e.preventDefault();93            advance();94          }95        }}96      >97        <h2 class="peach-onboarding-card-headline">98          {CARDS[card()]!.headline}99        </h2>100        <hr class="peach-onboarding-divider" />101        <For each={CARDS[card()]!.body}>102          {(para) => <p class="peach-onboarding-body">{para}</p>}103        </For>104        <Show when={CARDS[card()]!.warning}>105          <p class="peach-onboarding-warning">106            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>107            <span>{CARDS[card()]!.warning}</span>108          </p>109        </Show>110      </div>111112      <div class="peach-onboarding-nav">113        <div class="peach-onboarding-dots" aria-hidden="true">114          <For each={CARDS}>115            {(_, i) => (116              <div117                class="peach-onboarding-dot"118                classList={{ "is-active": i() === card() }}119              />120            )}121          </For>122        </div>123        <button124          class="peach-onboarding-next"125          onClick={(e) => {126            e.stopPropagation();127            advance();128          }}129          disabled={completing()}130        >131          {card() === CARDS.length - 1 ? "Get started →" : "next →"}132        </button>133      </div>134    </section>135  );136}