5c03d53d4f1d
remove Chrome favicon API — always show colored initials chip
- time
- 2026-05-21 15:22 UTC · 5 months ago
- machine
- koh2
- parent
- 4d29415ccbb6
↓ peach-browser-5c03d53d.tar.gzatlas at this save
files changed8 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 −33src/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={() => {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>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={() => {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>;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;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 }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 }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 /**