fb549556f757
add silver platter (State 1) for single-match — full-width Fill button with ✓ flash micro-confirmation, secondary copy ⓤ / copy ⓟ / dismiss row, Escape-to-dismiss handling
- time
- 2026-04-26 18:15 UTC · 6 months ago
- machine
- koh2
- parent
- 7aa9d2bbb445
↓ peach-browser-fb549556.tar.gzatlas at this save
files changed1 file
src/content/inject.ts+183 −2src/content/inject.ts+183 −2
@@ -524,6 +524,64 @@524524 .success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }525525 .success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }526526 527+ /* Silver platter (State 1 — single match) */528+ .silver-platter {529+ padding: 14px 14px 12px;530+ display: flex; flex-direction: column; gap: 6px;531+ }532+ .sp-title {533+ font-size: 14px; font-weight: 500; color: var(--peach-text);534+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;535+ line-height: 1.25;536+ }537+ .sp-username {538+ font-size: 12px; color: var(--peach-text-dim);539+ font-family: "IBM Plex Mono", monospace;540+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;541+ margin-bottom: 6px;542+ }543+ .sp-fill-btn {544+ width: 100%; padding: 11px; border-radius: 9px; border: none;545+ background: var(--peach-accent); color: #fff;546+ font-size: 13px; font-weight: 500; cursor: pointer;547+ font-family: Inter, sans-serif; letter-spacing: 0.01em;548+ display: flex; align-items: center; justify-content: center; min-height: 38px;549+ transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;550+ }551+ .sp-fill-btn:hover {552+ filter: brightness(1.1);553+ box-shadow: 0 4px 20px var(--peach-glow, transparent);554+ transform: translateY(-1px);555+ }556+ .sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }557+ .sp-fill-btn.filled {558+ background: #16a34a; transform: none; filter: none;559+ box-shadow: 0 4px 16px rgba(22,163,74,0.35);560+ cursor: default;561+ }562+ .sp-fill-btn:disabled { cursor: default; }563+ .sp-actions {564+ display: flex; gap: 4px; justify-content: space-between; align-items: center;565+ margin-top: 4px;566+ }567+ .sp-action-btn {568+ flex: 1; height: 28px; border: none; border-radius: 6px;569+ background: transparent; color: var(--peach-text-dim);570+ font-size: 12px; cursor: pointer;571+ transition: background 120ms ease, color 120ms ease;572+ font-family: Inter, sans-serif; letter-spacing: 0.04em;573+ display: inline-flex; align-items: center; justify-content: center; gap: 4px;574+ }575+ .sp-action-btn:hover:not(:disabled) {576+ background: color-mix(in srgb, var(--peach-text) 5%, transparent);577+ color: var(--peach-text);578+ }579+ .sp-action-btn:disabled { opacity: 0.3; cursor: default; }580+ .sp-action-btn.flashed {581+ color: var(--peach-accent);582+ background: color-mix(in srgb, var(--peach-accent) 12%, transparent);583+ }584+527585 /* Locked state */528586 .locked-wrap {529587 display: flex; flex-direction: column; align-items: stretch;@@ -600,7 +658,7 @@600658 601659 let query = "";602660 let focusedIndex = -1;603- let currentMode: "list" | "create" | "success" = "list";661+ let currentMode: "list" | "create" | "success" | "silver-platter" = "list";604662 let searchInput: HTMLInputElement | null = null;605663 606664 // Pre-capture the anchor field value for username prefill@@ -898,6 +956,123 @@898956 setTimeout(() => dismiss(), 1600);899957 }900958 959+ function flashActionBtn(btn: HTMLButtonElement) {960+ btn.classList.add("flashed");961+ setTimeout(() => btn.classList.remove("flashed"), 700);962+ }963+964+ function renderSilverPlatter(entry: SelectorEntry) {965+ container.innerHTML = "";966+ searchInput = null;967+ focusedIndex = -1;968+ currentMode = "silver-platter";969+970+ const domHdr = document.createElement("div");971+ domHdr.className = "domain-header";972+ const eyebrow = document.createElement("div");973+ eyebrow.className = "domain-eyebrow";974+ eyebrow.textContent = "Saved for this site";975+ const domName = document.createElement("div");976+ domName.className = "domain-name";977+ domName.textContent = options.domain ?? location.hostname.replace(/^www\./, "");978+ domHdr.append(eyebrow, domName);979+ container.appendChild(domHdr);980+981+ const platter = document.createElement("div");982+ platter.className = "silver-platter";983+984+ const titleEl = document.createElement("div");985+ titleEl.className = "sp-title";986+ titleEl.textContent = entry.title;987+ platter.appendChild(titleEl);988+989+ if (entry.username) {990+ const userEl = document.createElement("div");991+ userEl.className = "sp-username";992+ userEl.textContent = entry.username;993+ platter.appendChild(userEl);994+ }995+996+ const fillBtn = document.createElement("button");997+ fillBtn.className = "sp-fill-btn";998+ fillBtn.type = "button";999+ fillBtn.textContent = "Fill credentials";1000+ fillBtn.addEventListener("click", () => {1001+ if (fillBtn.disabled) return;1002+ fillBtn.disabled = true;1003+ fillBtn.classList.add("filled");1004+ fillBtn.innerHTML = checkSvgStr();1005+ // Fire the fill, then exit via the standard exit animation. The fill1006+ // itself is async (handled in index.ts) — by 400ms the form is filled1007+ // and the popup begins its exit. Total dwell from click to host1008+ // removal: 400ms + 130ms (exit animation) = 530ms.1009+ onSelect(entry.id);1010+ setTimeout(() => dismiss(), 400);1011+ });1012+ platter.appendChild(fillBtn);1013+1014+ const actions = document.createElement("div");1015+ actions.className = "sp-actions";1016+1017+ const copyU = document.createElement("button");1018+ copyU.className = "sp-action-btn";1019+ copyU.type = "button";1020+ copyU.title = "Copy username";1021+ copyU.textContent = "copy ⓤ";1022+ copyU.disabled = !entry.username;1023+ copyU.addEventListener("click", async (e) => {1024+ e.stopPropagation();1025+ if (!entry.username) return;1026+ let alreadyWritten = false;1027+ try { await navigator.clipboard.writeText(entry.username); alreadyWritten = true; } catch {}1028+ // Mirror the popup's copy path so the COPY message can clear the1029+ // clipboard via the configured timeout. alreadyWritten lets the SW1030+ // skip a redundant write when the page-level write succeeded.1031+ await chrome.runtime.sendMessage({1032+ type: "COPY",1033+ payload: { text: entry.username, label: "Username", alreadyWritten },1034+ }).catch(() => {});1035+ flashActionBtn(copyU);1036+ });1037+ actions.appendChild(copyU);1038+1039+ const copyP = document.createElement("button");1040+ copyP.className = "sp-action-btn";1041+ copyP.type = "button";1042+ copyP.title = "Copy password";1043+ copyP.textContent = "copy ⓟ";1044+ copyP.addEventListener("click", async (e) => {1045+ e.stopPropagation();1046+ const resp = await chrome.runtime.sendMessage({1047+ type: "GET_ENTRY_FOR_FILL",1048+ payload: { id: entry.id },1049+ }).catch(() => null) as { ok: boolean; data?: { password?: string } } | null;1050+ if (!resp?.ok || !resp.data?.password) return;1051+ let alreadyWritten = false;1052+ try { await navigator.clipboard.writeText(resp.data.password); alreadyWritten = true; } catch {}1053+ await chrome.runtime.sendMessage({1054+ type: "COPY",1055+ payload: { text: resp.data.password, label: "Password", alreadyWritten },1056+ }).catch(() => {});1057+ flashActionBtn(copyP);1058+ });1059+ actions.appendChild(copyP);1060+1061+ const closeBtn = document.createElement("button");1062+ closeBtn.className = "sp-action-btn";1063+ closeBtn.type = "button";1064+ closeBtn.title = "Dismiss";1065+ closeBtn.textContent = "✕";1066+ closeBtn.addEventListener("click", (e) => {1067+ e.stopPropagation();1068+ dismiss(true);1069+ });1070+ actions.appendChild(closeBtn);1071+1072+ platter.appendChild(actions);1073+ container.appendChild(platter);1074+ }1075+9011076 function renderLocked() {9021077 container.innerHTML = "";9031078 @@ -1020,7 +1195,7 @@10201195 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }10211196 return;10221197 }1023- if (currentMode === "create" || currentMode === "success") {1198+ if (currentMode === "create" || currentMode === "success" || currentMode === "silver-platter") {10241199 if (e.key === "Escape") { e.preventDefault(); dismiss(true); }10251200 return;10261201 }@@ -1073,6 +1248,12 @@10731248 10741249 if (options.locked) {10751250 renderLocked();1251+ } else if (entries.length === 1 && !options.isSignup) {1252+ // Silver platter (State 1): single domain match → focused fill view.1253+ // Multi-match goes through renderList instead. Signup mode always uses1254+ // the list because the user is choosing which existing identity to1255+ // surface — there's no single "fill credentials" interpretation.1256+ renderSilverPlatter(entries[0]!);10761257 } else {10771258 renderList();10781259 }