bce5897d91f7
redesign Import view to mirror Export menu pattern: card menu (peachscript paste / backup file) routes into focused subviews; PeachScript is the primary import path; preserve onboarding header, replace warning, master-password creation, merge/replace flows; bump 2.7.1
- time
- 2026-04-28 01:14 UTC · 6 months ago
- machine
- koh2
- parent
- 9d435183c145
↓ peach-browser-bce5897d.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/Import.tsx+304 −186src/popup/views/contracts.test.tsx+8 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.7.0",4+ "version": "2.7.1",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.7.0",4+ "version": "2.7.1",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.7.0",3+ "version": "2.7.1",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/views/Import.tsx+304 −186
@@ -16,6 +16,9 @@1616 import { parsePayload } from "../../crypto/peachPayload";1717 import type { VaultEntry } from "../../shared/types";1818 19+type ImportSubview = "menu" | "peachscript" | "file";20+type ImportMergeMode = "add" | "replace";21+1922 interface ImportProps {2023 mode?: "existing" | "onboarding";2124 replacesExistingVault?: boolean;@@ -23,83 +26,148 @@2326 onImport: () => void | Promise<void>;2427 }2528 26-type ImportMergeMode = "add" | "replace";27-2829 export function ImportView(props: ImportProps) {30+ const [view, setView] = createSignal<ImportSubview>("menu");31+ const mode = () => props.mode ?? "existing";32+ const isOnboarding = () => mode() === "onboarding";33+2934 return (30- <MainImportView31- mode={props.mode}32- replacesExistingVault={props.replacesExistingVault}33- onBack={props.onBack}34- onImport={props.onImport}35- />35+ <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">36+ <Show when={view() === "menu"}>37+ <ImportMenu38+ mode={mode()}39+ replacesExistingVault={props.replacesExistingVault}40+ onBack={props.onBack}41+ onPick={(v) => setView(v)}42+ />43+ </Show>44+ <Show when={view() === "peachscript"}>45+ <PeachScriptImport46+ mode={mode()}47+ replacesExistingVault={props.replacesExistingVault}48+ onBack={() => setView("menu")}49+ onImport={props.onImport}50+ />51+ </Show>52+ <Show when={view() === "file"}>53+ <FileImport54+ mode={mode()}55+ replacesExistingVault={props.replacesExistingVault}56+ onBack={() => setView("menu")}57+ onImport={props.onImport}58+ />59+ </Show>60+ </div>3661 );3762 }3863 39-function MainImportView(props: ImportProps) {40- const mode = () => props.mode ?? "existing";64+// ---------------------------------------------------------------------------65+// Menu66+// ---------------------------------------------------------------------------4167 42- // PeachScript paste flow68+function ImportMenu(props: {69+ mode: "existing" | "onboarding";70+ replacesExistingVault?: boolean;71+ onBack: () => void;72+ onPick: (view: ImportSubview) => void;73+}) {74+ const isOnboarding = props.mode === "onboarding";75+ return (76+ <>77+ <Show78+ when={isOnboarding}79+ fallback={<OverlayHeader title="import" onBack={props.onBack} />}80+ >81+ <div class="peach-lock-view-header">82+ <PeachWordmark class="peach-wordmark-wrap" />83+ <button class="peach-lock-restore-link" onClick={props.onBack}>84+ ‹ back85+ </button>86+ </div>87+ </Show>88+ <div class="peach-settings-body peach-export-menu">89+ <Show when={isOnboarding && props.replacesExistingVault}>90+ <div class="peach-settings-group">91+ <div class="peach-restore-warning">92+ Restoring now replaces the locked local vault on this device with93+ a fresh one from your backup or export file.94+ </div>95+ </div>96+ </Show>97+98+ <div class="peach-settings-group peach-export-card peach-export-card--primary">99+ <p class="peach-settings-label">peachscript</p>100+ <p class="peach-export-card-copy">101+ Paste a PeachScript backup from another Peach device.102+ {props.mode === "existing" ? " We'll merge it into your vault." : ""}103+ </p>104+ <button105+ class="peach-btn peach-btn--subtle peach-btn--sm"106+ onClick={() => props.onPick("peachscript")}107+ >108+ Paste PeachScript →109+ </button>110+ </div>111+112+ <div class="peach-settings-group peach-export-card">113+ <p class="peach-settings-label">backup file</p>114+ <p class="peach-export-card-copy">115+ A Peach JSON export, or a file from Bitwarden, Proton Pass,116+ 1Password, or a browser CSV (Chrome, Safari, Edge).117+ </p>118+ <button119+ class="peach-btn peach-btn--subtle peach-btn--sm"120+ onClick={() => props.onPick("file")}121+ >122+ Choose file →123+ </button>124+ </div>125+ </div>126+ </>127+ );128+}129+130+// ---------------------------------------------------------------------------131+// PeachScript paste flow132+// ---------------------------------------------------------------------------133+134+function PeachScriptImport(props: {135+ mode: "existing" | "onboarding";136+ replacesExistingVault?: boolean;137+ onBack: () => void;138+ onImport: () => void | Promise<void>;139+}) {43140 const [scriptInput, setScriptInput] = createSignal("");44141 const [scriptPass, setScriptPass] = createSignal("");45- const [scriptWorking, setScriptWorking] = createSignal(false);46- const [scriptError, setScriptError] = createSignal<string | null>(null);47- const [scriptResult, setScriptResult] = createSignal<{ entryCount: number } | null>(null);48-49- // File flow (existing format detection)50- const [preview, setPreview] = createSignal<ImportParseResult | null>(null);51- const [rawImport, setRawImport] = createSignal<string | null>(null);52- const [backupPassword, setBackupPassword] = createSignal("");53142 const [masterPassword, setMasterPassword] = createSignal("");54143 const [masterConfirm, setMasterConfirm] = createSignal("");55- const [importing, setImporting] = createSignal(false);144+ const [working, setWorking] = createSignal(false);56145 const [error, setError] = createSignal<string | null>(null);57- const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");58- const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);59- const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);60- const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);61- const [existingCount, setExistingCount] = createSignal<number>(0);146+ const [result, setResult] = createSignal<{ entryCount: number } | null>(null);62147 63- const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);64- const needsMasterPassword = createMemo(() => mode() === "onboarding");148+ const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));149+ const isOnboarding = props.mode === "onboarding";65150 const masterPasswordReady = createMemo(66- () =>67- masterPassword().length >= 8 && masterPassword() === masterConfirm()151+ () => masterPassword().length >= 8 && masterPassword() === masterConfirm()68152 );69153 70- const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));71-72- async function refreshExistingCount() {73- if (mode() !== "existing") return;74- const res = await sendMessage({ type: "GET_ENTRIES" });75- const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];76- setExistingCount(list.length);77- }78-79- void refreshExistingCount();80-81- // ---------------------------------------------------------------------82- // PeachScript flow83- // ---------------------------------------------------------------------84-85- async function handlePeachScriptImport() {86- setScriptError(null);87- setScriptResult(null);154+ async function handleImport() {155+ setError(null);156+ setResult(null);88157 if (!scriptLooksValid()) {89- setScriptError("Paste your PeachScript first.");158+ setError("Paste your PeachScript first.");90159 return;91160 }92161 if (!scriptPass().trim()) {93- setScriptError("Enter the backup password.");162+ setError("Enter the backup password.");94163 return;95164 }96- setScriptWorking(true);165+ setWorking(true);97166 try {98167 const bytes = decodePeachScript(normalisePeachScript(scriptInput()));99168 const payload = await parsePayload(bytes, scriptPass());100169 101- // Onboarding: create a fresh vault first.102- if (mode() === "onboarding") {170+ if (isOnboarding) {103171 if (!masterPasswordReady()) {104172 throw new Error("MASTER_PASSWORD_REQUIRED");105173 }@@ -127,20 +195,135 @@127195 }).catch(() => {});128196 }129197 130- setScriptResult({ entryCount: payload.entries.length });198+ setResult({ entryCount: payload.entries.length });131199 await props.onImport();132200 } catch (err) {133201 const code = err instanceof Error ? err.message : String(err);134- setScriptError(getImportErrorCopy(code));202+ setError(getImportErrorCopy(code));135203 } finally {136- setScriptWorking(false);204+ setWorking(false);137205 }138206 }139207 140- // ---------------------------------------------------------------------141- // File flow (Bitwarden / 1Password / etc.)142- // ---------------------------------------------------------------------208+ return (209+ <>210+ <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />211+ <div class="peach-settings-body">212+ <Show when={isOnboarding && props.replacesExistingVault}>213+ <div class="peach-settings-group">214+ <div class="peach-restore-warning">215+ Restoring now replaces the locked local vault on this device with216+ a fresh one from your backup.217+ </div>218+ </div>219+ </Show>220+ <div class="peach-settings-group peach-export-card peach-export-card--primary">221+ <p class="peach-settings-label">paste peachscript</p>222+ <p class="peach-export-card-copy">223+ {isOnboarding224+ ? "Paste a PeachScript backup from another Peach device to restore your vault."225+ : "We'll merge the entries into your existing vault."}226+ </p>227+ <textarea228+ class="peach-script-input"229+ placeholder="paste PeachScript here…"230+ value={scriptInput()}231+ onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}232+ />233+ <input234+ class="peach-restore-input"235+ type="password"236+ placeholder="PeachScript password"237+ value={scriptPass()}238+ onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}239+ />240+ <Show when={isOnboarding}>241+ <div class="peach-auth-grid">242+ <input243+ class="peach-restore-input"244+ type="password"245+ placeholder="new master password"246+ value={masterPassword()}247+ onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}248+ />249+ <input250+ class="peach-restore-input"251+ type="password"252+ placeholder="confirm master password"253+ value={masterConfirm()}254+ onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}255+ />256+ </div>257+ </Show>258+ <button259+ class="peach-btn peach-btn--subtle peach-btn--sm"260+ onClick={() => void handleImport()}261+ disabled={262+ working() ||263+ !scriptLooksValid() ||264+ !scriptPass().trim() ||265+ (isOnboarding && !masterPasswordReady())266+ }267+ >268+ {working() ? "decoding…" : "Restore from PeachScript"}269+ </button>270+ <Show when={error()}>271+ <div class="peach-restore-error">{error()}</div>272+ </Show>273+ <Show when={result()}>274+ <div class="peach-export-success">275+ <span class="peach-export-success-check">✓</span>276+ <span>277+ Restored {result()!.entryCount} entr278+ {result()!.entryCount === 1 ? "y" : "ies"}.279+ </span>280+ </div>281+ </Show>282+ </div>283+ </div>284+ </>285+ );286+}143287 288+// ---------------------------------------------------------------------------289+// File flow (Bitwarden / 1Password / Peach JSON / etc.)290+// ---------------------------------------------------------------------------291+292+function FileImport(props: {293+ mode: "existing" | "onboarding";294+ replacesExistingVault?: boolean;295+ onBack: () => void;296+ onImport: () => void | Promise<void>;297+}) {298+ const isOnboarding = props.mode === "onboarding";299+ const needsMasterPassword = () => isOnboarding;300+301+ const [preview, setPreview] = createSignal<ImportParseResult | null>(null);302+ const [rawImport, setRawImport] = createSignal<string | null>(null);303+ const [backupPassword, setBackupPassword] = createSignal("");304+ const [masterPassword, setMasterPassword] = createSignal("");305+ const [masterConfirm, setMasterConfirm] = createSignal("");306+ const [importing, setImporting] = createSignal(false);307+ const [error, setError] = createSignal<string | null>(null);308+ const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");309+ const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);310+ const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);311+ const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);312+ const [existingCount, setExistingCount] = createSignal<number>(0);313+314+ const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);315+ const masterPasswordReady = createMemo(316+ () => masterPassword().length >= 8 && masterPassword() === masterConfirm()317+ );318+319+ async function refreshExistingCount() {320+ if (isOnboarding) return;321+ const res = await sendMessage({ type: "GET_ENTRIES" });322+ const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];323+ setExistingCount(list.length);324+ }325+ void refreshExistingCount();326+144327 async function parseSource(text: string, password?: string) {145328 try {146329 const result = await parseImportSource(text, password);@@ -217,7 +400,7 @@217400 }218401 }219402 220- if (mode() === "existing" && mergeMode() === "replace" && !awaitingReplaceConfirm()) {403+ if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {221404 setAwaitingReplaceConfirm(true);222405 return;223406 }@@ -242,7 +425,7 @@242425 }243426 }244427 245- if (mode() === "existing" && mergeMode() === "replace") {428+ if (!isOnboarding && mergeMode() === "replace") {246429 const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });247430 if (!(clearRes as { ok: boolean }).ok) {248431 throw new Error("CLEAR_VAULT_FAILED");@@ -250,7 +433,7 @@250433 }251434 252435 let dupes = 0;253- if (mode() === "existing" && mergeMode() === "add") {436+ if (!isOnboarding && mergeMode() === "add") {254437 dupes = await countDuplicates(currentPreview.entries);255438 }256439 @@ -299,120 +482,20 @@299482 }300483 301484 return (302- <div class={mode() === "onboarding" ? "peach-lock-view" : "peach-overlay-view ov-enter-right"}>303- <Show304- when={mode() === "onboarding"}305- fallback={306- <div class="peach-overlay-header">307- <button308- class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"309- onClick={props.onBack}310- aria-label="Back"311- >312- <svg313- width="7" height="12" viewBox="0 0 7 12"314- fill="none" stroke="currentColor"315- stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"316- >317- <path d="M6 1L1 6L6 11" />318- </svg>319- </button>320- <span class="peach-overlay-title">import</span>321- <div class="peach-overlay-spacer"></div>322- </div>323- }324- >325- <div class="peach-lock-view-header">326- <PeachWordmark class="peach-wordmark-wrap" />327- <button328- class="peach-lock-restore-link"329- onClick={props.onBack}330- disabled={importing() || scriptWorking()}331- >332- ‹ back333- </button>334- </div>335- </Show>336-485+ <>486+ <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />337487 <div class="peach-settings-body">338- <Show when={mode() === "onboarding" && props.replacesExistingVault}>488+ <Show when={isOnboarding && props.replacesExistingVault}>339489 <div class="peach-settings-group">340490 <div class="peach-restore-warning">341491 Restoring now replaces the locked local vault on this device with342- a fresh one from your backup or export file.492+ a fresh one from your backup.343493 </div>344494 </div>345495 </Show>346-347- {/* PeachScript paste — primary path */}348- <div class="peach-settings-group peach-restore-group">349- <p class="peach-settings-label">paste peachscript</p>350- <p class="peach-restore-copy">351- {mode() === "onboarding"352- ? "Paste a PeachScript backup from another Peach device to restore your vault."353- : "Paste a PeachScript backup from another Peach device. We'll merge it into your vault."}354- </p>355- <textarea356- class="peach-script-input"357- placeholder="paste PeachScript here…"358- value={scriptInput()}359- onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}360- />361- <input362- class="peach-restore-input"363- type="password"364- placeholder="PeachScript password"365- value={scriptPass()}366- onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}367- style="margin-top:8px"368- />369- <Show when={mode() === "onboarding"}>370- <div class="peach-auth-grid" style="margin-top:8px">371- <input372- class="peach-restore-input"373- type="password"374- placeholder="new master password"375- value={masterPassword()}376- onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}377- />378- <input379- class="peach-restore-input"380- type="password"381- placeholder="confirm master password"382- value={masterConfirm()}383- onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}384- />385- </div>386- </Show>387- <div class="peach-settings-actions" style="margin-top:8px">388- <button389- class="peach-btn peach-btn--subtle peach-btn--sm"390- onClick={() => void handlePeachScriptImport()}391- disabled={392- scriptWorking() ||393- !scriptLooksValid() ||394- !scriptPass().trim() ||395- (mode() === "onboarding" && !masterPasswordReady())396- }397- >398- {scriptWorking() ? "decoding…" : "Restore from PeachScript"}399- </button>400- </div>401- <Show when={scriptError()}>402- <div class="peach-restore-error" style="margin-top:8px">{scriptError()}</div>403- </Show>404- <Show when={scriptResult()}>405- <div class="peach-export-success" style="margin-top:8px">406- <span class="peach-export-success-check">✓</span>407- <span>Restored {scriptResult()!.entryCount} entr{scriptResult()!.entryCount === 1 ? "y" : "ies"}.</span>408- </div>409- </Show>410- </div>411-412- {/* File import — for migrating from other managers */}413- <div class="peach-settings-group peach-restore-group">414- <p class="peach-settings-label">import a file</p>415- <p class="peach-restore-copy">496+ <div class="peach-settings-group peach-export-card peach-export-card--primary">497+ <p class="peach-settings-label">choose a file</p>498+ <p class="peach-export-card-copy">416499 Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO417500 credential-exchange passkeys, or a Peach JSON backup.418501 </p>@@ -420,10 +503,10 @@420503 <div class="peach-provider-chips">421504 <span class="peach-provider-chip">Peach JSON</span>422505 <span class="peach-provider-chip">FIDO passkeys</span>423- <span class="peach-provider-chip">Proton Pass CSV/JSON</span>424- <span class="peach-provider-chip">Bitwarden CSV/JSON</span>425- <span class="peach-provider-chip">1Password JSON</span>426- <span class="peach-provider-chip">Chrome / Edge / Safari CSV</span>506+ <span class="peach-provider-chip">Proton Pass</span>507+ <span class="peach-provider-chip">Bitwarden</span>508+ <span class="peach-provider-chip">1Password</span>509+ <span class="peach-provider-chip">Browser CSV</span>427510 </div>428511 429512 <input@@ -431,14 +514,13 @@431514 type="file"432515 accept=".csv,.json"433516 onChange={(event) => void handleFileSelect(event)}434- style="margin-top:8px"435517 />436518 </div>437519 438520 <Show when={needsBackupPassword()}>439521 <div class="peach-settings-group">440522 <p class="peach-settings-label">unlock backup</p>441- <p class="peach-restore-copy">523+ <p class="peach-export-card-copy">442524 Enter the password used when you created this encrypted Peach JSON443525 backup.444526 </p>@@ -503,7 +585,7 @@503585 </div>504586 </Show>505587 506- <Show when={mode() === "existing" && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>588+ <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>507589 <div class="peach-settings-group">508590 <p class="peach-settings-label">merge or replace</p>509591 <label class="peach-import-choice">@@ -547,7 +629,7 @@547629 <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>548630 <div class="peach-settings-group">549631 <p class="peach-settings-label">new master password</p>550- <p class="peach-restore-copy">632+ <p class="peach-export-card-copy">551633 This password unlocks the Peach vault created on this device after552634 import.553635 </p>@@ -590,7 +672,7 @@590672 591673 <Show when={duplicateWarning() > 0 && !importing()}>592674 <div class="peach-settings-group">593- <div class="peach-restore-copy">675+ <div class="peach-export-card-copy">594676 {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.595677 </div>596678 </div>@@ -610,7 +692,7 @@610692 >611693 {importing()612694 ? "importing..."613- : mode() === "onboarding"695+ : isOnboarding614696 ? `Create vault + import ${preview()!.entries.length}`615697 : mergeMode() === "replace"616698 ? awaitingReplaceConfirm()@@ -618,19 +700,55 @@618700 : `Replace vault with ${preview()!.entries.length}`619701 : `Import ${preview()!.entries.length}`}620702 </button>621- <button622- class="peach-btn peach-btn--ghost peach-btn--sm"623- onClick={props.onBack}624- disabled={importing()}625- >626- Cancel627- </button>628703 </div>629704 </div>630705 </Show>631-632706 </div>707+ </>708+ );709+}710+711+// ---------------------------------------------------------------------------712+// Shared headers713+// ---------------------------------------------------------------------------714+715+function OverlayHeader(props: { title: string; onBack: () => void }) {716+ return (717+ <div class="peach-overlay-header">718+ <button719+ class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"720+ onClick={props.onBack}721+ aria-label="Back"722+ >723+ <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">724+ <path d="M6 1L1 6L6 11" />725+ </svg>726+ </button>727+ <span class="peach-overlay-title">{props.title}</span>728+ <div class="peach-overlay-spacer"></div>633729 </div>730+ );731+}732+733+function SubviewHeader(props: {734+ title: string;735+ onBack: () => void;736+ mode: "existing" | "onboarding";737+}) {738+ // Onboarding sub-flows reuse the lock-screen header; existing-vault flows739+ // use the standard overlay back-arrow header.740+ return (741+ <Show742+ when={props.mode === "onboarding"}743+ fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}744+ >745+ <div class="peach-lock-view-header">746+ <PeachWordmark class="peach-wordmark-wrap" />747+ <button class="peach-lock-restore-link" onClick={props.onBack}>748+ ‹ back749+ </button>750+ </div>751+ </Show>634752 );635753 }636754 src/popup/views/contracts.test.tsx+8 −0
@@ -145,6 +145,14 @@145145 container: HTMLElement,146146 contents = '{"export":"test"}'147147 ) {148+ // The Import view now opens onto a menu — click into the file subview149+ // before locating the file input. Idempotent: a second click won't re-fire150+ // because the menu unmounts once the subview renders.151+ const fileCard = findButton(container, "Choose file →");152+ if (fileCard) {153+ fileCard.click();154+ await flushView();155+ }148156 const input = container.querySelector('input[type="file"]') as HTMLInputElement;149157 const file = new File([contents], "import.json", { type: "application/json" });150158 Object.defineProperty(input, "files", {