From e1f4cb3b2d246af1e05bfe00d29a74742024d536 Mon Sep 17 00:00:00 2001 From: oliver <54379826+ok@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:21:20 +0200 Subject: [PATCH 1/4] [test] Give the fields harness its keyboard provider ListControlsRow registers its Cmd+F command since the filter shortcut landed, so the fields layout case threw before measuring anything. --- test/frontend-layout/harness-fields-entry.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/frontend-layout/harness-fields-entry.tsx b/test/frontend-layout/harness-fields-entry.tsx index 46cecc91..52dbccd5 100644 --- a/test/frontend-layout/harness-fields-entry.tsx +++ b/test/frontend-layout/harness-fields-entry.tsx @@ -88,7 +88,9 @@ createRoot(document.getElementById('root') as HTMLElement).render(
- + + +
From fcfb24f3bfe2f3e0d442dd744c5c3ce80968b878 Mon Sep 17 00:00:00 2001 From: oliver <54379826+ok@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:21:25 +0200 Subject: [PATCH 2/4] [fix] Raise the copy icon to 3:1 in dark mode The icon was painted in the outline token, 2.1:1 on a field and 2.5:1 on a card in dark mode, below the 3:1 a graphic needs. It now uses the muted text token, which clears it in both themes. --- src/renderer/components/primitives/CopyButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/primitives/CopyButton.tsx b/src/renderer/components/primitives/CopyButton.tsx index 45946664..9dd242d5 100644 --- a/src/renderer/components/primitives/CopyButton.tsx +++ b/src/renderer/components/primitives/CopyButton.tsx @@ -36,7 +36,7 @@ export default function CopyButton({ value, className = '', showLabel = false }: title={label} className={`shrink-0 inline-flex items-center justify-center rounded-sm focus-ring ${className}`} > - + )} {copied ? t('actions.copied') : ''} From fd4fcada3e0ef7da7a9b5a4a07d6536ceebc2769 Mon Sep 17 00:00:00 2001 From: oliver <54379826+ok@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:21:25 +0200 Subject: [PATCH 3/4] [docs] Note that set-value leaves focus where it was A key pressed after an AX set-value lands on whatever held focus, so a scenario clicks a field before typing into it. --- .claude/lessons.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/lessons.md b/.claude/lessons.md index 0c4f2708..2410f69e 100644 --- a/.claude/lessons.md +++ b/.claude/lessons.md @@ -342,7 +342,8 @@ app is frontmost; `press` needs an AX-focused element the inactive app does not cannot tell dev instances apart (all are "Electron"). Keys go through the main-process channel (`sendInputEvent`, menu accelerators via their menu item). `sendInputEvent` only queues, so wait a rendered frame before the next step, and give Return/Space their `char` event — a focused button -activates on keypress, not keyDown. +activates on keypress, not keyDown. `set-value` does not move DOM focus either: a key pressed after +it lands on whatever held focus (a dialog's panel), so `click` the field before typing into it. **An inactive Electron window still activates the app on macOS 27 unless AppKit's state restorer is off.** The restorer runs at every launch even with no saved state and, when it finishes after From 82bc7e83e71d5989fddd5c4800620c023423b8dc Mon Sep 17 00:00:00 2001 From: oliver <54379826+ok@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:21:29 +0200 Subject: [PATCH 4/4] [feat] Paste, type and copy folder paths Folder fields showed the path as text, so a path could neither be pasted in nor copied out. Each is now an input: a typed path is taken on Enter or blur and checked as a picked one is; a fixed path is read-only with Copy. --- .claude/design.md | 49 +- .claude/mockups/path-field-input.html | 595 ++++++++++++++++++ CHANGELOG.md | 3 + eslint-rules/invariants.mjs | 10 +- src/main/backup-settings.js | 23 +- src/preload/preload.js | 1 + .../components/layout/SectionHeading.tsx | 6 +- .../components/modals/AddFolderShareModal.tsx | 8 +- .../components/modals/BackupSetupModal.tsx | 34 +- .../components/modals/EditFolderModal.tsx | 22 +- .../components/modals/EditSpaceModal.tsx | 13 +- .../components/modals/MirrorFolderModal.tsx | 3 +- .../components/modals/RestoreBackupModal.tsx | 28 +- .../components/path/MountPathField.tsx | 9 +- src/renderer/components/path/PathRow.tsx | 139 +++- src/renderer/locales/de/common.json | 2 +- src/renderer/locales/en/common.json | 2 +- src/renderer/locales/es/common.json | 2 +- src/renderer/locales/fr/common.json | 2 +- src/renderer/locales/it/common.json | 2 +- src/renderer/platform/global.d.ts | 11 +- .../screens/settings/BackupSettings.tsx | 34 +- .../screens/settings/StorageSettings.tsx | 32 +- src/renderer/styles/tailwind.css | 9 +- test/frontend-layout/README.md | 4 +- test/frontend-layout/harness-fields-entry.tsx | 14 +- .../harness-truncation-entry.tsx | 52 +- test/frontend-layout/run-truncation.mjs | 2 +- test/frontend/scenarios/s160-backup-setup.mjs | 18 + .../s194-managed-backup-required.mjs | 8 +- test/invariants/path-field-harmonized.test.js | 36 +- test/unit/backup-settings.test.js | 40 +- 32 files changed, 1043 insertions(+), 170 deletions(-) create mode 100644 .claude/mockups/path-field-input.html diff --git a/.claude/design.md b/.claude/design.md index 6a2b9725..99100de7 100644 --- a/.claude/design.md +++ b/.claude/design.md @@ -35,7 +35,8 @@ them to `var()`-backed utility colors, and a `.dark` class on `` swaps the - **Soft, heavy rounding.** The UI reads as stacked rounded cards; corner radii climb with element size (§5). - **App, not webpage.** Text is not user-selectable; copy is offered through an explicit - `` (`src/renderer/components/primitives/CopyButton.tsx`). + `` (`src/renderer/components/primitives/CopyButton.tsx`), or by a read-only field + where the value is one (a fixed path). - **Accessibility is a gate, not a finish.** Every interactive control carries a name/role/state; focus is always visible; motion respects the OS setting. See `.claude/testing.md`. - **Desktop-first, fixed-width.** Almost no responsive breakpoints — this is a windowed Electron @@ -665,8 +666,8 @@ places at the right end (the filter's count and clear button). ### Field label — `primitives/FieldLabel.tsx` `font-headline text-sm font-bold text-accent px-1`, and the element is the point: `htmlFor` renders -a `
- + {isOwner ? t('editFolder.sourceLabel') : t('editFolder.mirrorLabel')}

{!canRelocate ? t('editFolder.sourceFixed') : isOwner ? t('editFolder.sourceDesc') : t('editFolder.mirrorDesc')}

- {/* Same row as Add Folder and Mirror to Disk — a path is a path, whichever door you - came through. Display-only when the location is fixed: the field stays, the button - is what says whether you can re-point it. */} - + {canRelocate ? ( + + ) : ( + + )} {pathError && {pathError}}
diff --git a/src/renderer/components/modals/EditSpaceModal.tsx b/src/renderer/components/modals/EditSpaceModal.tsx index 47027c24..4a9c77c3 100644 --- a/src/renderer/components/modals/EditSpaceModal.tsx +++ b/src/renderer/components/modals/EditSpaceModal.tsx @@ -50,6 +50,11 @@ export default function EditSpaceModal({ space, onSave, onClose }: EditSpaceModa if (picked) setFolderEdit(picked) }, [folder]) + function handleTyped(typed: string) { + setFolderError(null) + setFolderEdit(typed) + } + async function handleSave() { if (!isValid || !hasChanges || saving) return setSaving(true) @@ -97,16 +102,16 @@ export default function EditSpaceModal({ space, onSave, onClose }: EditSpaceModa
- {t('editSpace.downloadFolder')} + {t('editSpace.downloadFolder')}

{t('editSpace.downloadFolderDesc')}

- {/* The same row Add Folder, Mirror to Disk, Edit Folder and Storage settings show — a - path is a path, whichever door you came through. */} {folderError && ( {folderError} diff --git a/src/renderer/components/modals/MirrorFolderModal.tsx b/src/renderer/components/modals/MirrorFolderModal.tsx index 735b3d98..32a19df9 100644 --- a/src/renderer/components/modals/MirrorFolderModal.tsx +++ b/src/renderer/components/modals/MirrorFolderModal.tsx @@ -136,11 +136,12 @@ export default function MirrorFolderModal({ )} { void wizard.browse() }} + onCommit={wizard.setMountPath} /> diff --git a/src/renderer/components/modals/RestoreBackupModal.tsx b/src/renderer/components/modals/RestoreBackupModal.tsx index 86814372..97644f8c 100644 --- a/src/renderer/components/modals/RestoreBackupModal.tsx +++ b/src/renderer/components/modals/RestoreBackupModal.tsx @@ -68,14 +68,15 @@ export default function RestoreBackupModal({ isOpen, onClose, onRestored, locked onClose() } - async function chooseFolder() { + // Picked or typed, the peek says whether the folder holds a backup. + async function openFolder(pick: () => Promise) { setError(null) try { - const picked = await window.bridge.browseBackupFolder() - if (!picked) return - setFolder(picked.folder) + const chosen = await pick() + if (!chosen) return + setFolder(chosen) setPeek(null) - setPeek(await request('backup:peek', { folder: picked.folder })) + setPeek(await request('backup:peek', { folder: chosen })) } catch (err) { setError(errorText(err)) } @@ -168,7 +169,7 @@ export default function RestoreBackupModal({ isOpen, onClose, onRestored, locked passphrase={passphrase} fieldError={fieldError} busy={busy} - onChooseFolder={() => void chooseFolder()} + onOpenFolder={(pick) => void openFolder(pick)} onPassphrase={(v) => { setPassphrase(v); setFieldError(null) }} /> ) : ( @@ -242,27 +243,34 @@ function SnapshotChoice({ snapshots, chosen, onChoose }: SnapshotChoiceProps) { ) } +async function pickBackupFolder() { + return (await window.bridge.browseBackupFolder())?.folder ?? null +} + interface SourceFieldsProps { folder: string | null peek: BackupPeek | null passphrase: string fieldError: string | null busy: boolean - onChooseFolder: () => void + /** Opens the folder `pick` answers: the picker's choice, or a typed path. */ + onOpenFolder: (pick: () => Promise) => void onPassphrase: (value: string) => void } // Where the backup is and what opens it. Nothing here can change while the dialog works on it. -function SourceFields({ folder, peek, passphrase, fieldError, busy, onChooseFolder, onPassphrase }: SourceFieldsProps) { +function SourceFields({ folder, peek, passphrase, fieldError, busy, onOpenFolder, onPassphrase }: SourceFieldsProps) { const { t } = useTranslation() const keyInFolder = !!peek?.keyCreatedAt return ( <>
- {t('restoreBackup.folderLabel')} + {t('restoreBackup.folderLabel')} onOpenFolder(pickBackupFolder)} + onCommit={(typed) => onOpenFolder(async () => typed)} subject={t('backup.folderSubject')} actionDisabled={busy} /> diff --git a/src/renderer/components/path/MountPathField.tsx b/src/renderer/components/path/MountPathField.tsx index c28866dd..baddba20 100644 --- a/src/renderer/components/path/MountPathField.tsx +++ b/src/renderer/components/path/MountPathField.tsx @@ -8,15 +8,14 @@ interface MountPathFieldProps { path: string error: string | null onBrowse: () => void + onCommit: (path: string) => void } -// The label is a carrying an id rather than a