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
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+21 −0src/popup/views/Vault.tsx+54 −2
manifest.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": {

view file

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",

view file

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": {

view file

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;

view file

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                         />

view file