a4900d4d1c92

add Onboarding view with three-card welcome flow

time
2026-04-28 04:41 UTC · 6 months ago
machine
koh2
parent
e056e5981747
1 file src/popup/views/Onboarding.tsx+136 −0
src/popup/views/Onboarding.tsx+136 −0
@@ -0,0 +1,136 @@1+import { For, Show, createSignal } from "solid-js";2+3+import { PeachWordmark } from "../components/PeachMark";4+import { RuneBackground } from "../components/RuneBackground";5+6+interface OnboardingCard {7+  headline: string;8+  body: string[];9+  warning: string | null;10+}11+12+const 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+];39+40+const CARD_EXIT_MS = 120;41+const CARD_ENTER_GAP_MS = 60;42+43+interface OnboardingProps {44+  onComplete: () => void | Promise<void>;45+}46+47+export function Onboarding(props: OnboardingProps) {48+  const [card, setCard] = createSignal(0);49+  const [exiting, setExiting] = createSignal(false);50+  const [completing, setCompleting] = createSignal(false);51+52+  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+  }72+73+  return (74+    <section75+      class="peach-screen peach-theme peach-onboarding"76+      classList={{ "is-completing": completing() }}77+    >78+      <RuneBackground opacity={0.07} />79+80+      <header class="peach-screen-header peach-onboarding-header">81+        <PeachWordmark class="peach-wordmark-wrap" />82+      </header>83+84+      <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>111+112+      <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+}

view file