diff --git a/README.md b/README.md index cac5ed1..81d4b62 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ archives are vendored under `vendor/zennotes/` with their source identity and checksums (`manifest.json`), and `package-lock.json` pins the complete install. A clean checkout installs them with `npm ci`, without a source clone or sibling repository. The vendored set is the core release -[core-2.55.0-core.h0743bd3af2a4204a](https://github.com/ZenNotes/zennotes/releases/tag/core-2.55.0-core.h0743bd3af2a4204a) -(desktop commit `00db7036`, clean tree). +[core-2.56.1-core.hc872c08b6872aec4](https://github.com/ZenNotes/zennotes/releases/tag/core-2.56.1-core.hc872c08b6872aec4) +(desktop commit `f8b24c09`, clean tree). `npm run boundaries:check` verifies the pins, installed versions, singleton React/CodeMirror peers, and public export usage. `npm run core:adopt -- ` diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 8f74548..5bcac66 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -596,12 +596,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 28; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ZenWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 1.14.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -725,12 +725,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 28; DEVELOPMENT_TEAM = WYY7PK57DM; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 1.14.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -747,12 +747,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 28; DEVELOPMENT_TEAM = WYY7PK57DM; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 1.14.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -767,12 +767,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 28; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 1.14.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -789,12 +789,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 28; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ZenWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 1.14.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -827,12 +827,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 27; + CURRENT_PROJECT_VERSION = 28; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 1.14.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/package-lock.json b/package-lock.json index 5a85ff7..58e33cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,9 @@ "@lezer/highlight": "^1.2.1", "@replit/codemirror-vim": "^6.4.0", "@xyflow/react": "^12.11.6", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.55.0-core.h0743bd3af2a4204a.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.55.0-boundaries.hd2f617179eb16ef2.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.55.0-boundaries.hd2f617179eb16ef2.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.56.1-core.hc872c08b6872aec4.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.56.1-boundaries.h3434125ac49b85c7.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.56.1-boundaries.h3434125ac49b85c7.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.15", "function-plot": "^1.25.3", @@ -3351,9 +3351,9 @@ } }, "node_modules/@zennotes/app-core": { - "version": "2.55.0-core.h0743bd3af2a4204a", - "resolved": "file:vendor/zennotes/zennotes-app-core-2.55.0-core.h0743bd3af2a4204a.tgz", - "integrity": "sha512-FuPj4V2Am8y9FctFhA4bLyH0OoiDBMApx4YoFx5j3e3yVNaU5oniCQJWzjBFqMQW7H2Nsf8nd7nbbm0iF4t4OA==", + "version": "2.56.1-core.hc872c08b6872aec4", + "resolved": "file:vendor/zennotes/zennotes-app-core-2.56.1-core.hc872c08b6872aec4.tgz", + "integrity": "sha512-kiNliezNdk0EOT0Ic2DnQTBoprlqXkMPijSVqku1fc/z1RqyS6m9Xff90TH8RP6kpbKe6fYojK8qGdgnTUTmYw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -3381,8 +3381,8 @@ "@myriaddreamin/typst.ts": "^0.7.0", "@replit/codemirror-vim": "^6.3.0", "@xyflow/react": "^12.11.2", - "@zennotes/bridge-contract": "2.55.0-boundaries.hd2f617179eb16ef2", - "@zennotes/shared-domain": "2.55.0-boundaries.hd2f617179eb16ef2", + "@zennotes/bridge-contract": "2.56.1-boundaries.h3434125ac49b85c7", + "@zennotes/shared-domain": "2.56.1-boundaries.h3434125ac49b85c7", "dompurify": "^3.3.4", "function-plot": "^1.25.3", "gray-matter": "^4.0.3", @@ -3426,18 +3426,18 @@ } }, "node_modules/@zennotes/bridge-contract": { - "version": "2.55.0-boundaries.hd2f617179eb16ef2", - "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.55.0-boundaries.hd2f617179eb16ef2.tgz", - "integrity": "sha512-DrLL7auzePYE/JzBTDX+S/LMbwWwd2chaYXiqunC2FL567bTsvEqdhAP9koKEjmxTBuoCxEFhu5+k3jhC2Jtcw==", + "version": "2.56.1-boundaries.h3434125ac49b85c7", + "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.56.1-boundaries.h3434125ac49b85c7.tgz", + "integrity": "sha512-W8XNcjixpGxdNHPs03xiyuA1Q/4ZnLcdX2O/ifEWLpZlqmwM9btdIochzsJlqU5eUZvlUFa7zibAlGFr2FRKzQ==", "license": "MIT" }, "node_modules/@zennotes/shared-domain": { - "version": "2.55.0-boundaries.hd2f617179eb16ef2", - "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.55.0-boundaries.hd2f617179eb16ef2.tgz", - "integrity": "sha512-CPD0OdmOufvanqcNV8ktOJraIVqQJ0tMpMZe7xYPxxssMupSwhqMx0NMve8/q+mYeln5LzQZ4Pmi33YUbdFfDA==", + "version": "2.56.1-boundaries.h3434125ac49b85c7", + "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.56.1-boundaries.h3434125ac49b85c7.tgz", + "integrity": "sha512-7/Qau3EgKTOIdx9BaOMWffvcISvP/cILDHd4y8eEqH3IkjUarWj7sVY1e9riuVODcGLCGo/6a8O1IUavCaF1ng==", "license": "MIT", "dependencies": { - "@zennotes/bridge-contract": "2.55.0-boundaries.hd2f617179eb16ef2", + "@zennotes/bridge-contract": "2.56.1-boundaries.h3434125ac49b85c7", "lz-string": "^1.5.0" } }, diff --git a/package.json b/package.json index 9493d94..0a657f9 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,9 @@ "vscode-oniguruma": "^2.0.1", "vscode-textmate": "^9.3.2", "zustand": "^5.0.15", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.55.0-core.h0743bd3af2a4204a.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.55.0-boundaries.hd2f617179eb16ef2.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.55.0-boundaries.hd2f617179eb16ef2.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.56.1-core.hc872c08b6872aec4.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.56.1-boundaries.h3434125ac49b85c7.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.56.1-boundaries.h3434125ac49b85c7.tgz", "@lezer/common": "^1.5.2" }, "devDependencies": { diff --git a/src/bridge/mobile-bridge.ts b/src/bridge/mobile-bridge.ts index 5217979..f1cbd86 100644 --- a/src/bridge/mobile-bridge.ts +++ b/src/bridge/mobile-bridge.ts @@ -39,6 +39,7 @@ import type { VaultTextSearchMatch } from '@zennotes/shared-domain/ipc' import { createDatabaseOps } from '@zennotes/shared-domain/database-ops' +import { normalizeVaultDisplayName, resolveVaultName } from '@zennotes/shared-domain/vault-display-name' import type { CustomCodeLanguage, CustomCodeLanguageInstallInput, @@ -58,7 +59,7 @@ import type { McpServerRuntime } from '@zennotes/shared-domain/mcp-clients' import { MobileVault } from './vault-fs' -import { listVaultDirs, VAULTS_DIR } from './native-fs' +import { listVaultDirs, readVaultDisplayName, readVaultDisplayNameAtUrl, VAULTS_DIR } from './native-fs' import { randomUUID } from './uuid' import { Directory, Filesystem } from '@capacitor/filesystem' import { @@ -166,7 +167,12 @@ export const EXTERNAL_VAULT_ROOT = 'zn://external-vault' export interface MobileVaultEntry { root: string + /** The folder's name on disk: what rename and delete act on, and what the + * open vault's `folderName` is compared with to mark it current. */ name: string + /** The name the vault goes by when its vault.json carries one (ZenNotes + * #692), normalized; the sheet shows it in place of the folder name. */ + displayName?: string tier: 'local' | 'icloud' | 'external' } @@ -210,17 +216,25 @@ async function looksLikeVaultDir(url: string): Promise { export async function listSwitchableVaults(): Promise { const out: MobileVaultEntry[] = [] for (const d of await listVaultDirs()) { - out.push({ root: `${VAULT_ROOT_PREFIX}${d.name}`, name: d.name, tier: 'local' }) + const displayName = normalizeVaultDisplayName(await readVaultDisplayName(d.name)) + out.push({ + root: `${VAULT_ROOT_PREFIX}${d.name}`, + name: d.name, + tier: 'local', + ...(displayName ? { displayName } : {}) + }) } const status = await icloudStatus().catch(() => null) if (status?.available && status.rootUrl) { for (const name of filterCloudVaultNames(status.vaults ?? [])) { const url = `${status.rootUrl}/${encodeURIComponent(name)}` if (!(await looksLikeVaultDir(url))) continue + const displayName = normalizeVaultDisplayName(await readVaultDisplayNameAtUrl(url)) out.push({ root: `${ICLOUD_VAULT_ROOT_PREFIX}${encodeURIComponent(name)}`, name, - tier: 'icloud' + tier: 'icloud', + ...(displayName ? { displayName } : {}) }) } } @@ -417,7 +431,45 @@ function currentVaultInfo(): VaultInfo | null { const remote = remoteVaultInfo() if (remote) return remote if (!vault) return null - return { root: friendlyVaultRoot(vault), name: vault.name } + // The root is a label, not a path, so the core cannot read the folder's + // name off it; folderName is what the display name falls back to and what + // the Vault name field shows as its placeholder (ZenNotes #692). + return { root: friendlyVaultRoot(vault), name: vault.name, folderName: vault.name } +} + +/** + * The open vault as the app names it: the display name from its vault.json + * when it has one (ZenNotes #692), else the folder name. Desktop resolves + * this in main (describeVault) before the renderer ever sees a VaultInfo, + * and the renderer only re-derives the name after a settings save or an + * external vault.json change, so a shell that handed over the folder name + * would show it until the first save. Every bridge method that returns the + * opened vault goes through here; a remote workspace's name comes from its + * server and passes untouched. The settings read is cached after the first + * open, and a vault whose vault.json cannot be read keeps its folder name, + * the way it always did. + */ +/** + * The open local vault's folder name, null in a remote workspace or before + * boot. The Vaults sheet lists folders and must mark the one that is open + * whatever the vault is called (rename and delete act on the folder), and + * the core's shell snapshot carries the vault's shown name, not its folder, + * so the sheet asks the bridge, which holds the MobileVault by folder name. + */ +export function currentVaultFolderName(): string | null { + if (remoteVaultInfo()) return null + return vault?.name ?? null +} + +async function describeCurrentVault(): Promise { + const info = currentVaultInfo() + if (!info || remoteVaultInfo() || !vault) return info + try { + const settings = await vault.getVaultSettings() + return { ...info, name: resolveVaultName(settings.displayName, info.name) } + } catch { + return info + } } /** @@ -463,7 +515,7 @@ async function openVaultByName(name: string, cloudRootUri: string | null = null) await next.open() vault = next localStorage.setItem(CURRENT_VAULT_KEY, name) - return currentVaultInfo() as VaultInfo + return (await describeCurrentVault()) as VaultInfo } /** @@ -913,25 +965,29 @@ export const mobileBridge: ZenBridge = { await disconnectRemote() // Reopen the remembered local tier so the app lands somewhere real. await openLocalVaultTier() - return currentVaultInfo() + return describeCurrentVault() }, // Mobile never boots into a broken workspace (an unreachable remote falls // back to the local vault in bootVault), so "retry" simply reports where // the app already landed. - retryWorkspaceBoot: async () => currentVaultInfo(), + retryWorkspaceBoot: () => describeCurrentVault(), listRemoteWorkspaceProfiles: () => listProfiles(), saveRemoteWorkspaceProfile: (input) => saveProfile(input), deleteRemoteWorkspaceProfile: (id) => deleteProfile(id), connectRemoteWorkspaceProfile: (id) => connectRemoteProfile(id), - getCurrentVault: async () => currentVaultInfo(), + getCurrentVault: () => describeCurrentVault(), listLocalVaults: async (): Promise => { const dirs = await listVaultDirs() - return dirs.map((d) => ({ - root: `${VAULT_ROOT_PREFIX}${d.name}`, - name: d.name, - lastOpenedAt: d.mtime - })) + // The switcher shows each vault by the name it goes by (#692): one small + // vault.json read per folder, the folder name when there is none. + return Promise.all( + dirs.map(async (d) => ({ + root: `${VAULT_ROOT_PREFIX}${d.name}`, + name: resolveVaultName(await readVaultDisplayName(d.name), d.name), + lastOpenedAt: d.mtime + })) + ) }, openLocalVault: async (root: string) => { // One entry point for switching to any device-reachable vault: local @@ -959,7 +1015,7 @@ export const mobileBridge: ZenBridge = { setStoragePref('local') return await openVaultByName(vaultNameFromRoot(root)) }, - closeVault: async () => currentVaultInfo(), + closeVault: () => describeCurrentVault(), pickVault: async () => { const picked = await pickExternalVault() if (!picked) return null diff --git a/src/bridge/native-fs.ts b/src/bridge/native-fs.ts index 199182f..6fa73fe 100644 --- a/src/bridge/native-fs.ts +++ b/src/bridge/native-fs.ts @@ -305,3 +305,38 @@ export async function listVaultDirs(): Promise<{ name: string; mtime: number }[] return [] } } + +/** + * The raw `displayName` a local vault carries in its own vault.json + * (ZenNotes #692), or null when the vault has none or the file cannot be + * read. For the vault switcher, which lists vaults that are not open and so + * have no MobileVault to ask; the caller normalizes and falls back to the + * folder name, the way desktop's describeVault does. + */ +export async function readVaultDisplayName(vaultName: string): Promise { + return readDisplayNameFrom({ + path: `${VAULTS_DIR}/${vaultName}/.zennotes/vault.json`, + directory: Directory.Documents + }) +} + +/** + * The same for a vault addressed by a file URL, which is how the iCloud tier + * lists its vaults (see looksLikeVaultDir). One read attempt, no download + * wait: an evicted vault.json answers null and the vault keeps its folder + * name in the switcher, rather than every listing waiting on iCloud. + */ +export async function readVaultDisplayNameAtUrl(url: string): Promise { + return readDisplayNameFrom({ path: `${url}/.zennotes/vault.json` }) +} + +async function readDisplayNameFrom(loc: { path: string; directory?: Directory }): Promise { + try { + const res = await Filesystem.readFile({ ...loc, encoding: Encoding.UTF8 }) + const parsed: unknown = JSON.parse(typeof res.data === 'string' ? res.data : '') + const name = (parsed as { displayName?: unknown } | null)?.displayName + return typeof name === 'string' ? name : null + } catch { + return null + } +} diff --git a/src/bridge/vault-lifecycle.test.ts b/src/bridge/vault-lifecycle.test.ts index 6f58675..45a8961 100644 --- a/src/bridge/vault-lifecycle.test.ts +++ b/src/bridge/vault-lifecycle.test.ts @@ -108,3 +108,20 @@ test('native absent-file reads propagate provider failures instead of allowing s const permission = new Error('Permission denied') await assert.rejects(NativeFs.prototype.readTextOrNull.call({ readText: async () => { throw permission } }, 'schema.json'), permission) }) +// ZenNotes #692: the desktop writes the vault's display name into vault.json. +// The phone's settings pass every key through, so a save from this device +// (toggling a favorite is enough) must keep the name, and a read must hand +// it to the core, which names the vault by it. +test('a vault display name written by desktop survives a phone settings save (ZenNotes #692)', async () => { + const { fs, vault } = fixture() + fs.files.set('.zennotes/vault.json', JSON.stringify({ + ...structuredClone(DEFAULT_VAULT_SETTINGS), displayName: 'Acme API docs' + })) + Object.assign(vault, { settingsCache: null }) + const settings = await vault.getVaultSettings() + assert.equal(settings.displayName, 'Acme API docs') + await vault.setVaultSettings({ ...settings, favorites: ['Notes/Work/One.md'] }) + const written = JSON.parse(fs.files.get('.zennotes/vault.json')!) + assert.equal(written.displayName, 'Acme API docs') + assert.deepEqual(written.favorites, ['Notes/Work/One.md']) +}) diff --git a/src/ui-mobile/MobileDrawer.tsx b/src/ui-mobile/MobileDrawer.tsx index 9f25d00..ec5ee54 100644 --- a/src/ui-mobile/MobileDrawer.tsx +++ b/src/ui-mobile/MobileDrawer.tsx @@ -32,6 +32,7 @@ import { ICLOUD_VAULT_ROOT_PREFIX, VAULT_ROOT_PREFIX, activeVaultStateKey, + currentVaultFolderName, listSwitchableVaults, renameVault, deleteVault, @@ -330,7 +331,14 @@ const TIER_SECTIONS = [ ] as const export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Element { - const currentName = useShellSnapshot().vault?.name ?? null + // The open vault is matched by its folder name, which is what an entry's + // `name` is: rename and delete act on the folder, so the guards below must + // hold for the folder that is open whatever the vault is called. The + // bridge names the folder; the snapshot's vault name is the display name + // (ZenNotes #692), which is what the rows show: the snapshot's for the + // open vault (the shell resolves it) and each entry's for the others. + const currentDisplayName = useShellSnapshot().vault?.name ?? null + const currentFolder = currentVaultFolderName() const { mode: workspaceMode, remoteProfileId, remoteProfiles } = useWorkspaceSnapshot() const [entries, setEntries] = useState(null) const [view, setView] = useState({ kind: 'list' }) @@ -358,7 +366,9 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele // whose friendly root string varies by provider. const currentTier = workspaceMode === 'remote' ? 'remote' : getStoragePref() const isCurrent = (e: MobileVaultEntry): boolean => - currentTier === e.tier && e.name === currentName + currentTier === e.tier && e.name === currentFolder + const shownName = (e: MobileVaultEntry): string => + (isCurrent(e) ? currentDisplayName : null) ?? e.displayName ?? e.name /** Switch flows close the sheet and drawer on success. */ const act = (key: string, fn: () => Promise): void => { @@ -490,13 +500,13 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele ) } > - {entry.name} + {shownName(entry)} {current && ✓}