Onboarding.tsx

4.6 KB · raw

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. No one but you can ever see your vault.",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      "Passwords, passkeys, cards, secure notes, two-factor codes.",32      "Autofills on every site. Generates strong passwords. Detects breaches.",33      "Free forever. Pro adds sync, passkeys, and advanced vault health.",34    ],35    warning: null,36  },37];3839const CARDS: OnboardingCard[] = BASE_CARDS;4041const CARD_EXIT_MS = 120;42const CARD_ENTER_GAP_MS = 60;4344interface OnboardingProps {45  onComplete: () => void | Promise<void>;46}4748export function Onboarding(props: OnboardingProps) {49  const [card, setCard] = createSignal(0);50  const [exiting, setExiting] = createSignal(false);51  const [completing, setCompleting] = createSignal(false);5253  async function advance() {54    if (exiting() || completing()) return;55    if (card() < CARDS.length - 1) {56      setExiting(true);57      window.setTimeout(() => {58        setCard((c) => c + 1);59        setExiting(false);60      }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);61    } else {62      setCompleting(true);63      // Don't persist peach_onboarded here. App.tsx persists it only when a64      // vault actually exists — so a user who quits mid-vault-creation65      // returns to the start of onboarding instead of getting stranded on66      // the create-vault screen with no back path.67      window.setTimeout(() => {68        void props.onComplete();69      }, 200);70    }71  }7273  function goBack() {74    if (exiting() || completing()) return;75    if (card() === 0) return;76    setExiting(true);77    window.setTimeout(() => {78      setCard((c) => c - 1);79      setExiting(false);80    }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);81  }8283  return (84    <section85      class="peach-screen peach-theme peach-onboarding"86      classList={{ "is-completing": completing() }}87    >88      <RuneBackground opacity={0.07} />8990      <header class="peach-screen-header peach-onboarding-header">91        <PeachWordmark class="peach-wordmark-wrap" />92      </header>9394      <div95        class="peach-onboarding-card"96        classList={{ "is-exiting": exiting() }}97        onClick={advance}98        role="button"99        tabIndex={0}100        onKeyDown={(e) => {101          if (e.key === "Enter" || e.key === " ") {102            e.preventDefault();103            advance();104          }105        }}106      >107        <h2 class="peach-onboarding-card-headline">108          {CARDS[card()]!.headline}109        </h2>110        <hr class="peach-onboarding-divider" />111        <For each={CARDS[card()]!.body}>112          {(para) => <p class="peach-onboarding-body">{para}</p>}113        </For>114        <Show when={CARDS[card()]!.warning}>115          <p class="peach-onboarding-warning">116            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>117            <span>{CARDS[card()]!.warning}</span>118          </p>119        </Show>120      </div>121122      <div class="peach-onboarding-nav">123        <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>124          <button125            class="peach-onboarding-back"126            onClick={(e) => {127              e.stopPropagation();128              goBack();129            }}130            disabled={completing()}131          >132            ← back133          </button>134        </Show>135        <div class="peach-onboarding-dots" aria-hidden="true">136          <For each={CARDS}>137            {(_, i) => (138              <div139                class="peach-onboarding-dot"140                classList={{ "is-active": i() === card() }}141              />142            )}143          </For>144        </div>145        <button146          class="peach-onboarding-next"147          onClick={(e) => {148            e.stopPropagation();149            advance();150          }}151          disabled={completing()}152        >153          {card() === CARDS.length - 1 ? "Get started →" : "next →"}154        </button>155      </div>156    </section>157  );158}