5c03d53d4f1d

remove Chrome favicon API — always show colored initials chip

time
2026-05-21 15:22 UTC · 5 months ago
machine
koh2
parent
4d29415ccbb6
8 files src/popup/App.tsx+7 −2src/popup/components/settings/StackCard.tsx+16 −4src/popup/components/settings/StackDeck.tsx+10 −2src/popup/components/settings/types.ts+1 −0src/popup/styles/popup.css+56 −0src/popup/styles/settings-stack.css+77 −0src/popup/views/Settings.tsx+46 −2src/popup/views/Vault.tsx+3 −33
src/popup/App.tsx+7 −2
@@ -1392,8 +1392,13 @@13921392                     const should = await sendMessage({ type: "SHOULD_PLAY_CELEBRATION" });13931393                     if ((should as { ok: boolean; data?: boolean }).data === true) {13941394                       await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });1395-                      setView("pro-celebration");1396-                      window.setTimeout(() => setView("settings"), 2500);1395+                      // When activating from Settings, the in-deck fan animation1396+                      // IS the celebration — skip the full-screen overlay so1397+                      // Settings isn't unmounted mid-animation.1398+                      if (view() !== "settings") {1399+                        setView("pro-celebration");1400+                        window.setTimeout(() => setView("settings"), 2500);1401+                      }13971402                     }13981403                   }}13991404                   onBack={() => {

view file

src/popup/components/settings/StackCard.tsx+16 −4
@@ -1,3 +1,4 @@1+import { Show } from "solid-js";12 import type { JSX } from "solid-js";23 import { CardIcon } from "./CardIcon";34 import type { StackCardSpec } from "./types";@@ -7,6 +8,7 @@78   active: boolean;89   focused?: boolean;910   concealed?: boolean;11+  celebrating?: boolean;1012   top: number;1113   zIndex: number;1214   height: number;@@ -17,7 +19,7 @@1719 export function StackCard(props: StackCardProps) {1820   return (1921     <article20-      class={`stk-card ${props.active ? "is-active" : "is-tab"} ${props.focused ? "is-focused" : ""} ${props.concealed ? "is-concealed" : ""}`}22+      class={`stk-card ${props.active ? "is-active" : "is-tab"} ${props.focused ? "is-focused" : ""} ${props.concealed ? "is-concealed" : ""} ${props.celebrating ? "is-celebrating" : ""}`}2123       data-settings-card-id={props.spec.id}2224       data-active={props.active ? "true" : "false"}2325       role={props.active || props.concealed ? undefined : "button"}@@ -45,9 +47,19 @@4547     >4648       <div class="stk-card-top">4749         <span class="stk-card-num">{props.spec.num}</span>48-        <span class="stk-card-icon">49-          <CardIcon kind={props.spec.icon} />50-        </span>50+        <Show51+          when={props.spec.onIconClick}52+          fallback={<span class="stk-card-icon"><CardIcon kind={props.spec.icon} /></span>}53+        >54+          <button55+            class="stk-card-icon stk-card-icon--btn"56+            type="button"57+            aria-label="Celebrate"58+            onClick={(e) => { e.stopPropagation(); props.spec.onIconClick!(); }}59+          >60+            <CardIcon kind={props.spec.icon} />61+          </button>62+        </Show>5163       </div>5264       <h2 class="stk-card-title">{props.spec.title}</h2>5365       <p class="stk-card-lede">{props.spec.lede}</p>

view file

src/popup/components/settings/StackDeck.tsx+10 −2
@@ -1,4 +1,4 @@1-import { createMemo, createSignal, For, onMount, onCleanup } from "solid-js";1+import { createEffect, createMemo, createSignal, For, onMount, onCleanup } from "solid-js";22 import { StackCard } from "./StackCard";33 import type { CardId, StackBodies, StackCardSpec } from "./types";44 @@ -10,6 +10,9 @@1010   bodies: StackBodies;1111   /** Extension version string (e.g. "3.6.38"), shown next to the card counter. */1212   version?: string;13+  isFanning?: boolean;14+  celebratingCardId?: CardId | null;15+  isPro?: boolean;1316 }1417 1518 const EXPANDED_H = 258;@@ -31,6 +34,10 @@3134     }3235   }3336 37+  createEffect(() => {38+    if (props.isFanning) setDeckOpen(true);39+  });40+3441   onMount(() => {3542     const listener = () => handleBack();3643     window.addEventListener("peach:settings-back", listener);@@ -79,7 +86,7 @@7986 8087       <div8188         ref={(el) => { deckEl = el; }}82-        class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}89+        class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"} ${props.isFanning ? "is-fanning" : ""} ${props.isPro ? "is-pro" : ""}`}8390         onWheel={(e) => {8491           const cardEl = (e.target as HTMLElement).closest<HTMLElement>("[data-settings-card-id]");8592           if (!cardEl || cardEl.dataset.active !== "true") return;@@ -119,6 +126,7 @@119126                 active={active()}120127                 focused={!deckOpen() && active()}121128                 concealed={concealed()}129+                celebrating={!props.isFanning && props.celebratingCardId === card.id}122130                 top={top()}123131                 zIndex={zIndex()}124132                 onClick={() => {

view file

src/popup/components/settings/types.ts+1 −0
@@ -26,6 +26,7 @@2626   bgVar: string;2727   fgVar: string;2828   icon: StackIconKind;29+  onIconClick?: () => void;2930 }3031 3132 export type StackBodies = Record<CardId, JSX.Element>;

view file

src/popup/styles/popup.css+56 −0
@@ -6378,6 +6378,62 @@63786378   cursor: default;63796379 }63806380 6381+/* ── Pro card activation sparkles & thank-you ── */6382+button.peach-pro-active-mark-wrap {6383+  background: transparent;6384+  border: 0;6385+  padding: 0;6386+  cursor: pointer;6387+  position: relative;6388+  overflow: visible;6389+}6390+6391+.peach-pro-sparks {6392+  position: absolute;6393+  inset: 0;6394+  pointer-events: none;6395+  overflow: visible;6396+}6397+6398+.peach-pro-spark {6399+  position: absolute;6400+  top: 50%;6401+  left: 50%;6402+  font-size: 9px;6403+  line-height: 1;6404+  opacity: 0;6405+  transform: translate(-50%, -50%);6406+  animation: peach-pro-spark-fly 1.4s ease-out var(--spark-delay, 0ms) both;6407+}6408+6409+@keyframes peach-pro-spark-fly {6410+  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }6411+  25%  { opacity: 1; }6412+  100% {6413+    opacity: 0;6414+    transform: translate(6415+      calc(-50% + var(--spark-x, 0px)),6416+      calc(-50% + var(--spark-y, -40px))6417+    ) scale(0.8);6418+  }6419+}6420+6421+.peach-pro-thankyou-text {6422+  margin: 4px 0 0;6423+  font-size: 0.6875rem;6424+  font-style: italic;6425+  color: currentColor;6426+  opacity: 0;6427+  animation: peach-pro-thankyou-flash 2.5s ease both;6428+}6429+6430+@keyframes peach-pro-thankyou-flash {6431+  0%   { opacity: 0; transform: translateY(4px); }6432+  15%  { opacity: 0.75; transform: translateY(0); }6433+  75%  { opacity: 0.75; }6434+  100% { opacity: 0; }6435+}6436+63816437 /* ── Pro advanced health remediation queue ──────────────────── */63826438 .peach-health-pro-queue {63836439   display: flex;

view file

src/popup/styles/settings-stack.css+77 −0
@@ -527,6 +527,7 @@527527 528528 .stk-deck.is-open .stk-card.is-active .peach-pro-active-mark-wrap {529529   margin-bottom: 0;530+  overflow: visible;530531 }531532 532533 .stk-deck.is-open .stk-card.is-active .peach-pro-active-mark {@@ -630,4 +631,80 @@630631 [data-motion="reduce"] .stk-card,631632 [data-motion="reduce"] .stk-card-body {632633   transition: none !important;634+}635+636+/* ── Fan animation: fast card transitions during the pro celebration cycle ── */637+.stk-deck.is-fanning .stk-card {638+  transition:639+    transform 70ms ease,640+    height 70ms ease,641+    box-shadow 70ms ease,642+    opacity 70ms ease;643+}644+645+/* ── Card shake when the pro card lands after the fan ── */646+.stk-card.is-celebrating {647+  transition: none !important;648+  animation: stk-card-celebrate 680ms cubic-bezier(0.34, 1.56, 0.64, 1) both;649+}650+651+@keyframes stk-card-celebrate {652+  0%   { transform: translateY(var(--stk-card-top)); }653+  22%  { transform: translateY(calc(var(--stk-card-top) - 16px)) rotate(-2.5deg); }654+  44%  { transform: translateY(calc(var(--stk-card-top) + 7px)) rotate(1.8deg); }655+  66%  { transform: translateY(calc(var(--stk-card-top) - 7px)) rotate(-1.2deg); }656+  82%  { transform: translateY(calc(var(--stk-card-top) + 3px)); }657+  100% { transform: translateY(var(--stk-card-top)); }658+}659+660+/* ── Ambient sparkle dust on the Pro card when user is Pro ── */661+.stk-deck.is-pro .stk-card[data-settings-card-id="pro"]::before {662+  content: "";663+  position: absolute;664+  inset: 0;665+  border-radius: inherit;666+  pointer-events: none;667+  background-image:668+    radial-gradient(circle, rgba(255, 248, 220, 0.9) 0.5px, transparent 0.5px),669+    radial-gradient(circle, rgba(255, 255, 255, 0.7) 0.5px, transparent 0.5px),670+    radial-gradient(circle, rgba(255, 238, 170, 0.6) 1px, transparent 1px);671+  background-size: 38px 38px, 53px 47px, 71px 63px;672+  background-position: 0 0, 19px 15px, 9px 30px;673+  animation: stk-pro-dust 14s linear infinite;674+  opacity: 0.11;675+  z-index: 0;676+  mix-blend-mode: screen;677+}678+679+@keyframes stk-pro-dust {680+  0%   { transform: translate(0, 0); }681+  100% { transform: translate(53px, 47px); }682+}683+684+[data-motion="reduce"] .stk-deck.is-pro .stk-card[data-settings-card-id="pro"]::before,685+[data-motion="reduce"] .stk-card.is-celebrating {686+  animation: none;687+}688+689+/* ── Clickable card icon (easter egg trigger) ── */690+.stk-card-icon--btn {691+  background: transparent;692+  border: 0;693+  color: inherit;694+  padding: 0;695+  cursor: pointer;696+  opacity: 0.85;697+  display: inline-flex;698+  align-items: center;699+  justify-content: center;700+}701+702+.stk-card-icon--btn:hover {703+  opacity: 1;704+}705+706+/* ── Chip highlight clipping fix ── */707+/* .stk-card-body has overflow-x: hidden which clips the active chip's glow */708+.stk-card .peach-feedback-chips {709+  padding-left: 3px;633710 }

view file

src/popup/views/Settings.tsx+46 −2
@@ -149,6 +149,25 @@149149 150150   createEffect(() => { props.onCardChange?.(activeCard()); });151151 152+  const [justActivated, setJustActivated] = createSignal(false);153+  const [isFanning, setIsFanning] = createSignal(false);154+155+  function triggerCelebration() {156+    if (isFanning()) return;157+    setIsFanning(true);158+    setActiveCard("pro");159+    const fanOrder: CardId[] = ["protection", "vault", "sync", "appearance", "shortcuts", "feedback"];160+    fanOrder.forEach((id, i) => {161+      window.setTimeout(() => setActiveCard(id), (i + 1) * 80);162+    });163+    window.setTimeout(() => {164+      setActiveCard("pro");165+      setIsFanning(false);166+      setJustActivated(true);167+      window.setTimeout(() => setJustActivated(false), 2800);168+    }, (fanOrder.length + 1) * 80);169+  }170+152171   const [checkingBreaches, setCheckingBreaches] = createSignal(false);153172   const [breachResult, setBreachResult] = createSignal<string | null>(null);154173   const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);@@ -167,6 +186,7 @@167186       bgVar: "--stack-pro-bg",168187       fgVar: "--stack-pro-fg",169188       icon: "spark",189+      onIconClick: isPro() ? triggerCelebration : undefined,170190     },171191     {172192       id: "protection",@@ -1558,6 +1578,7 @@15581578       await handleActivateLicense();15591579       if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {15601580         await props.onLicenseChange?.();1581+        triggerCelebration();15611582       }15621583     }15631584 @@ -1649,11 +1670,31 @@16491670         >16501671           <div class="peach-pro-active">16511672             <div class="peach-pro-active-hero">1652-              <div class="peach-pro-active-mark-wrap">1673+              <button1674+                class="peach-pro-active-mark-wrap"1675+                type="button"1676+                aria-label="Celebrate"1677+                onClick={() => triggerCelebration()}1678+              >16531679                 <PeachMark class="peach-pro-active-mark" />1654-              </div>1680+                <Show when={justActivated()}>1681+                  <div class="peach-pro-sparks" aria-hidden="true">1682+                    <span class="peach-pro-spark" style="--spark-x:0px;--spark-y:-54px;--spark-delay:0ms">✦</span>1683+                    <span class="peach-pro-spark" style="--spark-x:38px;--spark-y:-38px;--spark-delay:50ms">✦</span>1684+                    <span class="peach-pro-spark" style="--spark-x:54px;--spark-y:0px;--spark-delay:110ms">·</span>1685+                    <span class="peach-pro-spark" style="--spark-x:38px;--spark-y:38px;--spark-delay:30ms">✦</span>1686+                    <span class="peach-pro-spark" style="--spark-x:0px;--spark-y:54px;--spark-delay:80ms">·</span>1687+                    <span class="peach-pro-spark" style="--spark-x:-38px;--spark-y:38px;--spark-delay:130ms">✦</span>1688+                    <span class="peach-pro-spark" style="--spark-x:-54px;--spark-y:0px;--spark-delay:55ms">·</span>1689+                    <span class="peach-pro-spark" style="--spark-x:-38px;--spark-y:-38px;--spark-delay:95ms">✦</span>1690+                  </div>1691+                </Show>1692+              </button>16551693               <h2 class="peach-pro-active-title">Peach Pro</h2>16561694               <p class="peach-pro-active-since">{formatActivatedSince(licenseInfo()?.issued)}</p>1695+              <Show when={justActivated()}>1696+                <p class="peach-pro-thankyou-text" aria-live="polite">thank you ✦</p>1697+              </Show>16571698             </div>16581699 16591700             <Show when={licenseKey()}>@@ -1830,6 +1871,9 @@18301871             cards={cards()}18311872             bodies={bodies}18321873             version={version}1874+            isFanning={isFanning()}1875+            celebratingCardId={justActivated() ? "pro" : null}1876+            isPro={isPro()}18331877           />18341878         </div>18351879       }

view file

src/popup/views/Vault.tsx+3 −33
@@ -1,7 +1,6 @@11 import { createEffect, createMemo, createSignal, For, Show } from "solid-js";22 import type { VaultEntry } from "../../shared/types";33 import { extractDomain, getDisplayDomain } from "../../shared/domain";4-import { faviconUrl } from "../../shared/favicon";54 import { popupCopy } from "../copy";65 76 function vibrate(ms: number) {@@ -9,38 +8,9 @@98 }109 1110 function FaviconIcon(props: { domain: string; title?: string; isNote: boolean; isPasskey: boolean }) {12-  const src = props.domain && !props.isNote && !props.isPasskey13-    ? faviconUrl(props.domain)14-    : null;15-  // Note/passkey entries keep their semantic glyphs. Login entries get a16-  // colored-initials chip when the real favicon hasn't loaded — better than17-  // a black square on freshly-imported entries the browser hasn't visited18-  // yet (Chrome's `_favicon/` cache is empty for unvisited domains).19-  const fallback = props.isPasskey20-    ? <PasskeyIcon />21-    : props.isNote22-      ? <NoteIcon />23-      : <InitialsChip title={props.title ?? props.domain ?? ""} />;24-  const [failed, setFailed] = createSignal(false);25-  return (26-    <Show when={src && !failed()} fallback={fallback}>27-      <img28-        src={src!}29-        width="16"30-        height="16"31-        alt=""32-        style="border-radius:3px;"33-        onError={() => setFailed(true)}34-        onLoad={(e) => {35-          // Chrome's _favicon/ API returns the default globe at its native36-          // 16×16 even when size=32 is requested. A real cached favicon37-          // arrives at ≥32px naturally. Treat ≤16 as "no favicon" so the38-          // InitialsChip renders instead of the globe.39-          if ((e.currentTarget as HTMLImageElement).naturalWidth <= 16) setFailed(true);40-        }}41-      />42-    </Show>43-  );11+  if (props.isPasskey) return <PasskeyIcon />;12+  if (props.isNote) return <NoteIcon />;13+  return <InitialsChip title={props.title ?? props.domain ?? ""} />;4414 }4515 4616 /**

view file