From c5d709da5aefd1a0c73d7ea43bfdb44908a6fe60 Mon Sep 17 00:00:00 2001 From: jagdeep sidhu Date: Fri, 24 Apr 2026 15:21:43 -0700 Subject: [PATCH 1/2] improve(vault): make invalid import rows actionable Let users jump directly to or remove invalid and duplicate paste lines from the validation list so line-number errors are easier to correct. Made-with: Cursor --- src/App.css | 38 +++++++ src/components/VaultImportModal.js | 136 +++++++++++++++++++----- src/components/VaultImportModal.test.js | 31 ++++++ 3 files changed, 176 insertions(+), 29 deletions(-) diff --git a/src/App.css b/src/App.css index 1cc2757..3fb8941 100644 --- a/src/App.css +++ b/src/App.css @@ -2910,6 +2910,16 @@ font-size: 0.9rem; } +.import-row--actionable { + cursor: pointer; +} + +.import-row--actionable:hover, +.import-row--actionable:focus-visible { + border-color: rgba(86, 132, 243, 0.32); + box-shadow: 0 0 0 2px rgba(86, 132, 243, 0.08); +} + .import-row--invalid { background: rgba(229, 107, 85, 0.06); border-color: rgba(229, 107, 85, 0.22); @@ -2931,6 +2941,7 @@ display: flex; flex-direction: column; gap: 2px; + flex: 1; min-width: 0; word-break: break-all; } @@ -2946,6 +2957,33 @@ font-size: 0.82rem; } +.import-row__remove { + width: 24px; + height: 24px; + border: 1px solid var(--panel-outline, rgba(0, 0, 0, 0.08)); + border-radius: 999px; + background: var(--panel); + color: var(--muted); + cursor: pointer; + flex-shrink: 0; + font: inherit; + font-size: 0.8rem; + line-height: 1; + opacity: 0; + transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.import-row:hover .import-row__remove, +.import-row:focus-within .import-row__remove { + opacity: 1; +} + +.import-row__remove:hover, +.import-row__remove:focus-visible { + border-color: rgba(229, 107, 85, 0.5); + color: #e56b55; +} + @keyframes vault-modal-in { from { opacity: 0; diff --git a/src/components/VaultImportModal.js b/src/components/VaultImportModal.js index af5be8f..4706e92 100644 --- a/src/components/VaultImportModal.js +++ b/src/components/VaultImportModal.js @@ -76,6 +76,16 @@ function rowStatusClass(row) { return 'is-negative'; } +function lineBounds(text, lineNo) { + const lines = String(text || '').split('\n'); + if (lineNo < 1 || lineNo > lines.length) return null; + let start = 0; + for (let i = 0; i < lineNo - 1; i += 1) { + start += lines[i].length + 1; + } + return { start, end: start + lines[lineNo - 1].length }; +} + export default function VaultImportModal({ open, onClose }) { const vault = useVault(); const { user } = useAuth(); @@ -117,6 +127,39 @@ export default function VaultImportModal({ open, onClose }) { [vault.data] ); + const selectPasteLine = useCallback( + (lineNo) => { + const bounds = lineBounds(paste, lineNo); + if (!bounds || !pasteRef.current) return; + const textarea = pasteRef.current; + textarea.focus(); + textarea.setSelectionRange(bounds.start, bounds.end); + + const lineHeight = + parseFloat(window.getComputedStyle(textarea).lineHeight) || 20; + textarea.scrollTop = Math.max( + 0, + (lineNo - 1) * lineHeight - textarea.clientHeight / 2 + ); + }, + [paste] + ); + + const removePasteLine = useCallback( + (lineNo) => { + const lines = paste.split('\n'); + if (lineNo < 1 || lineNo > lines.length) return; + lines.splice(lineNo - 1, 1); + const nextPaste = lines.join('\n'); + applyPastePreview(nextPaste); + setPaste(nextPaste); + setTimeout(() => { + if (pasteRef.current) pasteRef.current.focus(); + }, 0); + }, + [applyPastePreview, paste] + ); + const canSave = !saving && !validating && @@ -343,36 +386,71 @@ export default function VaultImportModal({ open, onClose }) { ) : null} ) : null} diff --git a/src/components/VaultImportModal.test.js b/src/components/VaultImportModal.test.js index f1ea6a2..dab9fd6 100644 --- a/src/components/VaultImportModal.test.js +++ b/src/components/VaultImportModal.test.js @@ -184,6 +184,37 @@ describe('VaultImportModal — paste → validate', () => { // Nothing is importable — Save remains disabled. expect(screen.getByTestId('vault-import-save')).toBeDisabled(); }); + + test('clicking an invalid row selects the offending textarea line', async () => { + const vault = unlockedVault(); + mount({ vault }); + const textarea = screen.getByTestId('vault-import-paste'); + const text = `${VALID_WIF_1},MN 1\n${INVALID_WIF},bad row`; + pasteInto(textarea, text); + await waitForValidationDone(); + + const invalidRow = screen.getAllByTestId('vault-import-row')[1]; + await userEvent.click(invalidRow); + + const lineStart = text.indexOf(INVALID_WIF); + expect(textarea.selectionStart).toBe(lineStart); + expect(textarea.selectionEnd).toBe(text.length); + }); + + test('remove line action deletes the offending row from the paste', async () => { + const vault = unlockedVault(); + mount({ vault }); + const textarea = screen.getByTestId('vault-import-paste'); + pasteInto(textarea, `${VALID_WIF_1},MN 1\n${INVALID_WIF},bad row`); + await waitForValidationDone(); + + await userEvent.click(screen.getByLabelText('Remove line 2')); + + expect(textarea).toHaveValue(`${VALID_WIF_1},MN 1`); + await waitForValidationDone(); + expect(screen.getAllByTestId('vault-import-row')).toHaveLength(1); + expect(screen.getByTestId('vault-import-save')).not.toBeDisabled(); + }); }); describe('VaultImportModal — save flow (UNLOCKED)', () => { From 63e719181ed3afe9f8a97eb6f6c054e607240b05 Mon Sep 17 00:00:00 2001 From: jagdeep sidhu Date: Fri, 24 Apr 2026 15:31:33 -0700 Subject: [PATCH 2/2] fix(vault): keep remove-line keyboard access Ignore bubbled key events from row children so the remove-line button remains keyboard accessible while row shortcuts still select source lines. Made-with: Cursor --- src/components/VaultImportModal.js | 1 + src/components/VaultImportModal.test.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/components/VaultImportModal.js b/src/components/VaultImportModal.js index 4706e92..06bcfb5 100644 --- a/src/components/VaultImportModal.js +++ b/src/components/VaultImportModal.js @@ -402,6 +402,7 @@ export default function VaultImportModal({ open, onClose }) { onKeyDown={ canEditSource ? (e) => { + if (e.target !== e.currentTarget) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectPasteLine(r.lineNo); diff --git a/src/components/VaultImportModal.test.js b/src/components/VaultImportModal.test.js index dab9fd6..fa5bdc1 100644 --- a/src/components/VaultImportModal.test.js +++ b/src/components/VaultImportModal.test.js @@ -215,6 +215,23 @@ describe('VaultImportModal — paste → validate', () => { expect(screen.getAllByTestId('vault-import-row')).toHaveLength(1); expect(screen.getByTestId('vault-import-save')).not.toBeDisabled(); }); + + test('row keyboard shortcut ignores key events from the remove button', async () => { + const vault = unlockedVault(); + mount({ vault }); + const textarea = screen.getByTestId('vault-import-paste'); + const text = `${VALID_WIF_1},MN 1\n${INVALID_WIF},bad row`; + pasteInto(textarea, text); + await waitForValidationDone(); + + const removeButton = screen.getByLabelText('Remove line 2'); + removeButton.focus(); + fireEvent.keyDown(removeButton, { key: ' ' }); + + expect(document.activeElement).toBe(removeButton); + expect(textarea.selectionStart).toBe(text.length); + expect(textarea.selectionEnd).toBe(text.length); + }); }); describe('VaultImportModal — save flow (UNLOCKED)', () => {