b83ac298112c

show animated RuneCanvas on the PeachScript import form when valid script is pasted, swapping out the textarea; bump 4.10.26

time
2026-08-21 02:16 UTC · 2 months ago
machine
koh2
agent
qwen3.8-max
session
peachscript import rune canvas
parent
9a5ff2570867
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+7 −0src/popup/views/Lock.tsx+57 −41tests/lock-screen.spec.ts+8 −4
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.10.25",4+  "version": "4.10.26",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "4.10.25",5+  "version": "4.10.26",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "4.10.25",3+  "version": "4.10.26",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/popup/styles/popup.css+7 −0
@@ -4083,6 +4083,13 @@40834083   display: block;40844084 }40854085 4086+/* Animated rune preview shown once valid PeachScript is pasted — mirrors the4087+   export screen's RuneCanvas. */4088+.peach-restore-rune-preview {4089+  overflow: hidden;4090+  border-radius: 8px;4091+}4092+40864093 /* ── Codex source ─────────────────────────────────────────────────────── */40874094 40884095 .peach-restore-codex-copy {

view file

src/popup/views/Lock.tsx+57 −41
@@ -4,6 +4,7 @@44 import { RecoveryWordEntryView } from "./RecoveryWordEntry";55 import { RecoveryWordDisplayView } from "./RecoveryWordDisplay";66 import { RuneBackground } from "../components/RuneBackground";7+import { RuneCanvas } from "../components/RuneCanvas";78 import { storageGet, storageRemove, storageSet } from "../../shared/storage";89 import { isPeachScript, decodePeachScript, normalisePeachScript } from "../../crypto/peachScript";910 import { parsePayload } from "../../crypto/peachPayload";@@ -1085,48 +1086,63 @@10851086         />10861087       </Show>10871088       <Show when={animating() === null}>1088-        <textarea1089-          class="peach-restore-script"1090-          placeholder="paste peachscript…"1091-          rows={3}1092-          aria-label="Paste your PeachScript backup"1093-          value={scriptInput()}1094-          onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}1095-        />1096-        <div class="peach-restore-or" aria-hidden="true">or</div>1097-        <label1098-          class="peach-btn peach-btn--ghost peach-btn--sm peach-restore-file-btn"1099-          onClick={(e) => {1100-            // In the detached window the dialog can open directly — let the1101-            // label's real user gesture activate the input. In the action1102-            // popup we must detach first (the dialog would dismiss the popup).1103-            if (restoreDeepLinkActive(RESTORE_DEEP_LINKS.symbols)) return;1104-            e.preventDefault();1105-            void detachForFilePicker(RESTORE_DEEP_LINKS.symbols).then((detached) => {1106-              if (!detached) peachFileRef?.click();1107-            });1108-          }}1089+        <Show1090+          when={scriptValid()}1091+          fallback={1092+            <>1093+              <textarea1094+                class="peach-restore-script"1095+                placeholder="paste peachscript…"1096+                rows={3}1097+                aria-label="Paste your PeachScript backup"1098+                value={scriptInput()}1099+                onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}1100+              />1101+              <div class="peach-restore-or" aria-hidden="true">or</div>1102+              <label1103+                class="peach-btn peach-btn--ghost peach-btn--sm peach-restore-file-btn"1104+                onClick={(e) => {1105+                  if (restoreDeepLinkActive(RESTORE_DEEP_LINKS.symbols)) return;1106+                  e.preventDefault();1107+                  void detachForFilePicker(RESTORE_DEEP_LINKS.symbols).then((detached) => {1108+                    if (!detached) peachFileRef?.click();1109+                  });1110+                }}1111+              >1112+                open .peach file1113+                <input1114+                  ref={(el) => { peachFileRef = el; }}1115+                  type="file"1116+                  accept=".peach,text/plain"1117+                  style="display:none"1118+                  onChange={async (e) => {1119+                    const file = (e.currentTarget as HTMLInputElement).files?.[0];1120+                    if (!file) return;1121+                    try {1122+                      setScriptInput(await file.text());1123+                    } catch {1124+                      setError("Couldn't read file.");1125+                    } finally {1126+                      (e.currentTarget as HTMLInputElement).value = "";1127+                    }1128+                  }}1129+                />1130+              </label>1131+              <p class="peach-restore-drop-hint">or drop the file anywhere here</p>1132+            </>1133+          }11091134         >1110-          open .peach file1111-          <input1112-            ref={(el) => { peachFileRef = el; }}1113-            type="file"1114-            accept=".peach,text/plain"1115-            style="display:none"1116-            onChange={async (e) => {1117-              const file = (e.currentTarget as HTMLInputElement).files?.[0];1118-              if (!file) return;1119-              try {1120-                setScriptInput(await file.text());1121-              } catch {1122-                setError("Couldn't read file.");1123-              } finally {1124-                (e.currentTarget as HTMLInputElement).value = "";1125-              }1126-            }}1127-          />1128-        </label>1129-        <p class="peach-restore-drop-hint">or drop the file anywhere here</p>1135+          <div class="peach-restore-rune-preview">1136+            <RuneCanvas runeString={scriptInput()} width={256} height={120} />1137+          </div>1138+          <button1139+            type="button"1140+            class="peach-restore-picker-back"1141+            onClick={() => { setScriptInput(""); setError(null); }}1142+          >1143+            ‹ paste again1144+          </button>1145+        </Show>11301146         <div class="peach-newvault-field">11311147           <label class="peach-label" for="restore-backup-pass">11321148             backup password

view file

tests/lock-screen.spec.ts+8 −4
@@ -367,10 +367,14 @@367367     });368368     await detached.waitForSelector(".peach-restore-script", { timeout: 15000 });369369     expect(detached.url()).toContain("restore=peachscript");370-    await detached.locator(".peach-restore-file-btn").click();371-    // The runes land in the script field, pass PeachScript validation, and a372-    // passphrase unlocks the restore button.373-    await expect(detached.locator(".peach-restore-script")).not.toHaveValue("", { timeout: 15000 });370+    // autoPick (best-effort) opens the picker ~50ms after mount; if the371+    // platform blocks it, click the button with a real gesture instead.372+    try {373+      await expect(detached.locator(".peach-rune-canvas")).toBeVisible({ timeout: 3000 });374+    } catch {375+      try { await detached.locator(".peach-restore-file-btn").click({ timeout: 5000 }); } catch { /* autoPick raced */ }376+    }377+    await expect(detached.locator(".peach-rune-canvas")).toBeVisible({ timeout: 15000 });374378     await detached.locator("#restore-backup-pass").fill("backup-passphrase-probe");375379     await expect(detached.locator(".peach-restore-submit")).toBeEnabled({ timeout: 15000 });376380     await ctx.close();

view file