e397baee42fc
favicon fallback: render colored title-initials chip when the real favicon isn't cached yet (login entries only); deterministic HSL hash so the same entry keeps the same color across renders; bump v2.11.10
- time
- 2026-04-29 01:41 UTC · 6 months ago
- machine
- koh2
- parent
- f45fccde9c90
↓ peach-browser-e397baee.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+21 −0src/popup/views/Vault.tsx+54 −2manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.11.9",4+ "version": "2.11.10",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.11.9",4+ "version": "2.11.10",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.11.9",3+ "version": "2.11.10",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/styles/popup.css+21 −0
@@ -1346,6 +1346,27 @@13461346 border-color: rgba(220,80,60,0.3);13471347 color: rgba(220,80,60,0.7);13481348 }1349+1350+/* Title-initials chip used as the favicon fallback for login entries whose1351+ * real favicon hasn't been cached by the browser yet (freshly-imported,1352+ * never-visited domains). Background colour is hashed from the title so1353+ * the same entry always shows the same chip. The chip fits the same1354+ * 26×26 row icon slot the favicon would take. */1355+.peach-favicon-initials {1356+ display: inline-flex;1357+ align-items: center;1358+ justify-content: center;1359+ width: 100%;1360+ height: 100%;1361+ border-radius: 4px;1362+ font-family: "IBM Plex Mono", monospace;1363+ font-size: 9px;1364+ font-weight: 600;1365+ letter-spacing: 0.04em;1366+ color: rgba(240, 232, 216, 0.92);1367+ text-transform: uppercase;1368+ user-select: none;1369+}13491370 .peach-entry-row-info { flex: 1; min-width: 0; }13501371 .peach-entry-row-name {13511372 display: block;src/popup/views/Vault.tsx+54 −2
@@ -3,11 +3,19 @@33 import { extractDomain, getDisplayDomain } from "../../shared/domain";44 import { faviconUrl } from "../../shared/favicon";55 6-function FaviconIcon(props: { domain: string; isNote: boolean; isPasskey: boolean }) {6+function FaviconIcon(props: { domain: string; title?: string; isNote: boolean; isPasskey: boolean }) {77 const src = props.domain && !props.isNote && !props.isPasskey88 ? faviconUrl(props.domain)99 : null;10- const fallback = props.isPasskey ? PASSKEY_ICON : props.isNote ? NOTE_ICON : LOGIN_ICON;10+ // Note/passkey entries keep their semantic glyphs. Login entries get a11+ // colored-initials chip when the real favicon hasn't loaded — better than12+ // a black square on freshly-imported entries the browser hasn't visited13+ // yet (Chrome's `_favicon/` cache is empty for unvisited domains).14+ const fallback = props.isPasskey15+ ? PASSKEY_ICON16+ : props.isNote17+ ? NOTE_ICON18+ : <InitialsChip title={props.title ?? props.domain ?? ""} />;1119 const [failed, setFailed] = createSignal(false);1220 return (1321 <Show when={src && !failed()} fallback={fallback}>@@ -23,6 +31,49 @@2331 );2432 }2533 34+/**35+ * Title → "PM"-style initials. First letters of the first two words,36+ * falling back to the first two characters of a single-word title. Empty37+ * titles get a small dot. Deterministic so the same entry always renders38+ * the same chip.39+ */40+function titleInitials(source: string): string {41+ const cleaned = source.replace(/^https?:\/\//, "").replace(/^www\./, "").trim();42+ if (!cleaned) return "·";43+ const words = cleaned.split(/[\s.\-_/]+/).filter(Boolean);44+ if (words.length >= 2 && words[0]![0] && words[1]![0]) {45+ return (words[0]![0]! + words[1]![0]!).toUpperCase();46+ }47+ const single = words[0] ?? cleaned;48+ return single.slice(0, 2).toUpperCase();49+}50+51+/**52+ * Hash a string to an HSL background. Fixed saturation + lightness so the53+ * palette stays muted and dark enough that white initials read clearly on54+ * top, while still giving each entry a recognisable colour signature.55+ */56+function titleBackground(source: string): string {57+ let hash = 0;58+ for (let i = 0; i < source.length; i++) {59+ hash = ((hash << 5) - hash + source.charCodeAt(i)) | 0;60+ }61+ const hue = ((hash % 360) + 360) % 360;62+ return `hsl(${hue}, 32%, 26%)`;63+}64+65+function InitialsChip(props: { title: string }) {66+ return (67+ <span68+ class="peach-favicon-initials"69+ style={{ background: titleBackground(props.title) }}70+ aria-hidden="true"71+ >72+ {titleInitials(props.title)}73+ </span>74+ );75+}76+2677 const LOGIN_ICON = (2778 <svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round" aria-hidden="true">2879 <rect x="2" y="5" width="7" height="5" rx="1.2" />@@ -452,6 +503,7 @@452503 fallback={453504 <FaviconIcon454505 domain={entry.url ? getDisplayDomain(entry.url) : ""}506+ title={entry.title}455507 isNote={isNote}456508 isPasskey={isPasskey}457509 />