feat(vault): detail sidebar, role-grouped toolbar and fleet-style selection - #479
Conversation
Clicking a row now opens the secret in an NcAppSidebar over the vault list instead of navigating to a separate page, so the list context (folder, search, pagination) stays visible and intact behind it. The detail page is gone: the id rides as an optional segment on the list routes (/secrets/:id? and /folders/:folderId/:id?), because CnPageRenderer keys its render on the page id and a separate page remounted the list on every open. Deep links to #/secrets/<id> keep resolving (list + open sidebar) and stay behind the vault-lock guard. Route <-> sidebar mapping lives in src/utils/detailRoute.js. The sidebar lays the detail out Proton Pass style: the action row (labeled Edit opening the edit dialog, icon-only Share with ariaLabel, and a "Secret actions" menu holding Move, Delete and Close - the native X is hidden while the row renders; Esc still closes everywhere) sits with the title in the header; fields render as grouped icon/label/value boxes with a visible attachments box and a last-modified/created metadata box (NcDateTime); the informational tail folds into "More information" (sharing, requests, activity) and owner-only "Advanced" (versions, rotation, honey) disclosures. Card and identity secrets render their payloads as first-class rows (masked number/CVV/PIN/BSN with eye + copy, revealed numbers regrouped in blocks of four, identity split into Personal/Address/Contact sections); the old CardDisplay/IdentityDisplay components are deleted and the raw key row is hidden for composite types. The pane widens to clamp(300px, 35vw, 560px) above the library's mobile breakpoint. Decryption semantics are unchanged. Edit/Move refresh the list behind the sidebar; Delete closes it. The SecretDetail vitest specs are retargeted at the sidebar component; the new route mapping has its own spec; e2e drives Move/Share through the new action row and opens the More-information disclosure for the activity assertion; the guards spec drops the removed page id.
New source strings from the sidebar restyle - "Details", "Versions", "Secret actions", "More information", and the identity section headings "Personal details" / "Address details" / "Contact details" - translated into English plus all 36 required locales, with the .js catalogs regenerated and the parity ratchet tightened to record the progress.
Opening the folder the "..." stand-in node represents lit TWO rows in the rail: the node itself (vue-router exact-active styling on its link) and its parent, which received the deepest-visible-ancestor highlight. The ancestor rule now returns nothing when the active folder is the single hidden child the "..." stands for, so exactly one row reads as selected. Navigation through the node is unchanged, and the multi-hidden-children case keeps highlighting the parent row as before.
Navigating to another folder used to keep the previous folder's rows on screen until the new ones landed, then swap them mid-air - CnIndexPage only shows its full loading spinner while it is loading AND has nothing to render. A folderSwitching flag now blanks the list for the duration of the navigation's fetch, so a switch reads as spinner -> new folder's contents. The flag starts true because root <-> folder navigations REMOUNT the view (the routes are different manifest pages and CnPageRenderer keys its render on the page id), and a fresh mount rendered the store's previous rows before mounted() fetched anything - the folderId watcher only covers folder -> folder moves. In-folder refreshes and searches keep the in-place behavior.
A sweep over every locale for values identical to their English source
found six bulk-copied strings: "Applications awaiting approval", "No
applications awaiting approval" (31 locales), "Your secrets, folders
and applications at a glance", "Vault overview", "Rotation due" (33
each) and "View all" (31). All now carry real translations phrased
with each locale's established terminology. Script-mismatched
loanwords are also fixed: Token and Status become Cyrillic in mk/sr,
and Link becomes Länk/Ligação/Colliaziun in sv/pt/rm. Values that
already held a genuine translation were never touched, and legitimate
loanwords (URL, Token in Latin-script locales, Dashboard, Widgets,
Layout) and the brand string "Keepiq {version}" stay as they are. The
.js catalogs are regenerated.
The secret types come from the server with English labels seeded by SeedSecretTypes.php, and the frontend printed type.label raw - so the type filter, the create and edit dialogs' type selectors and the detail sidebar's type subline showed English in every language. A new secretTypeLabel() helper (src/utils/secretTypes.js) routes the label through the translator at all four sites, falling through unchanged for custom types without a catalog entry - the dynamic-key pattern the nav already uses for manifest menu labels. The ten system labels are translated in English plus all 36 required locales (295 new entries; seven keys were missing from every catalog), the .js catalogs are regenerated and the parity ratchet is tightened accordingly.
… l10n commit Commit 17988a7 accidentally carried a local development aid into LockScreen.vue: a DEV-ONLY auto-unlock block that reads the master password in plaintext from localStorage and unlocks the vault on page load, plus its status banner and data field. It was guarded by a NODE_ENV check (webpack strips it from production builds) and inert without an explicit localStorage opt-in, but it defeats the lock screen's security model and was never meant to be tracked. The same commit also dropped two @SPEC tags from handlePasskeyUnlock; they are restored. The file now matches its pre-17988a77 state exactly.
…p-row toolbar The vault list adopts CnIndexPage's own selection: selectable with rowClickToView (clicking opens the detail, checkboxes select), selectedIds fed from the bulk store and @select synced back. The table gets header select-all, row checkboxes and selected-row highlight; cards get a checkbox each; the list keeps shift-range selection through NcCheckboxRadioSwitch — the same Nextcloud checkbox the other views render — with a capture-phase shift recorder. One selection is shared across view switches, pruned to the visible rows on every list change (folder navigation, page flips, filters, refresh), and the strip's bulk buttons disable while the list is (re)loading so a stale selection from the previous page cannot be moved or deleted. Bulk actions live in the library's contextual selection strip alone (live count announced via role="status" — WCAG 2.1 SC 4.1.3 — plus Move, Share, Add to team folder and Delete with icons, and the strip's own Clear; bulk-actions §3.1's required bar). The Actions menu keeps only the Select-all checkbox — the sole whole-view select-all for the list and card views. Selected list rows tint the item with its hover token, never the checkbox gutter. Folders left the collection: vaults/subfolders render as a captioned strip (Vaults at root / Folders inside) above the collection in every view mode (the library's #before-collection slot), so they no longer masquerade as secrets in table/cards and survive pagination; the strip owns the single divider. Breadcrumbs render below the actions bar, above that strip, a step larger — the trail is the folder heading now that the page title is visually hidden (the h1 stays for accessibility). The bar owns the top row: keepiq's doubled page padding is dropped and the bar clears the floating nav toggle with a 30px !important margin — a margin so its background starts after the toggle, !important because design-system themes flatten the bar's box with their own !important rules. Keepiq-scoped, as is the container-scale rounding; other apps keep their heading and the library defaults. The secret-type filter is a funnel button beside the search field carrying the sort radios too; it flips to its filled glyph in the primary color whenever a type filter is set or the sort deviates from the default name sort. New l10n string "Filter and sort" in en + all 36 required locales. e2e: clickOverflowAction opens the bar menu via a new openActionsMenu helper; export-gdpr's four inline menu openers use it.
… display cap Being in a folder deeper than the rail renders (e.g. test11 under the 5-level cap) highlighted the deepest visible ancestor (test5), which read as the wrong folder being open. The active trail is computed once (activeFolderTrail); when it exceeds the cap no ancestor row lights up and the "…" node under the deepest visible branch renders active instead (ellipsisHighlightId, passed into NavFolderTree's ellipsis item) — exactly one row selected, and it is the row that stands for the hidden chain. This subsumes the earlier single-hidden-child special case.
ImageMagick's internal SVG renderer — used by Nextcloud theming to generate the favicon — does not draw stroked paths, so the shackle vanished from generated favicons. All five icons (app, app-dark, app-store, pwa-icon, pwa-icon-maskable) redraw the shackle as a filled outline of equivalent thickness, so stroke-less rasterizers render the full lock.
CI's stylelint (declaration-empty-line-before) rejects a blank line between a custom property and the following declaration — a formatting habit that is valid in nextcloud-vue's stylelint config but not in keepiq's. The full stylelint sweep is green again, and RESTYLE-PLAN's per-stage verification list now names stylelint so the gate stops being skippable locally.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ❌ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 550/550 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | 🚨 NO VERDICT — enabled but never ran | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-28 13:34 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 550/550 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | 🚨 NO VERDICT — enabled but never ran | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-28 13:55 UTC
Download the full PDF report from the workflow artifacts.
gate-16: @SPEC tags on the seven changed methods the gate named — onRowCheck/onSelectAll (bulk-actions multi-select requirement), the selectedFolderId watcher (folder management), typeFilterOptions and both secret dialogs' typeOptions (secret types), and the detail sidebar's mounted() (read secret). gate-40: the list checkbox's ariaLabel binding becomes aria-label — identical Vue behaviour, and the form-label rule recognises the kebab form. gate-45: the sidebar's accordion-chevron transition gains the prefers-reduced-motion fallback the rest of the file already has. gate-46: five @SPEC tags pointed at anchors that never existed — requirement-secret-sharing is requirement-share-a-secret (user-sharing) and requirement-cached-vault-is-read-only is requirement-offline-mode-is-strictly-read-only (offline cache).
Brings the released library features the vault page consumes, until now only available through a locally built dist overlaid into node_modules: the contextual selection strip (live role="status" count, #selection-actions slot, clear-selection wiring), the #after-search and #before-collection CnIndexPage slots, the role-grouped actions-bar layout with its narrow-width container-query tiers, clickToView on CnCardGrid/CnObjectCard, and the CnPageRenderer fix that stops lifted manifest fields (title, icon) falling through as HTML attributes on custom pages — the "Vault" tooltip that hovered over every custom page. With the package live, the overlay workflow is retired for this line of work: node_modules now matches the lockfile again.
…yword CI's stylelint (declaration-property-value-keyword-no-deprecated, in the newer toolchain the merged lockfile installs) rejects `word-break: break-word` — a deprecated keyword that always aliased overflow-wrap behaviour. The detail rows' value wrapping now uses `overflow-wrap: anywhere`, which wraps long secret values identically.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ❌ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 536/536 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | 🚨 NO VERDICT — enabled but never ran | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-31 08:25 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 536/536 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | 🚨 NO VERDICT — enabled but never ran | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-31 08:33 UTC
Download the full PDF report from the workflow artifacts.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Solide restyle. Route-driven state, security-hygiene in orde (geen v-html, reveal-state reset over secrets, fail-closed isOwner, warning-boven-waarde bij possiblyCompromisedAt), accessibility handhaaft WCAG 4.1.2 (icon-only buttons dragen ariaLabel + title), selection-pruning voorkomt cross-view leaks. Sinds mijn eerdere in-chat review is de nc-vue 2.27.0 dep bump geland (commit 8d262a80) — de blocker die het aan draft hield.
Wat sterk is:
- Docblock-discipline — Elke method + computed heeft
@spec openspec/...referenties (opsx-workflow). Comments verklaren consequent "waarom", niet "wat". possiblyCompromisedAtwarning boven de waarde — Correct BEFORE de user het wachtwoord kopieert. Niet dismissible. Goede security-UX.- Reveal-state resets in
secretIdwatcher (lines 1311-1314) —cardRevealed+bsnRevealednaarfalsebij row-switch. Belangrijke edge-case correct. PasswordFieldgebruikt:resolvecallback — decrypted key on-demand, niet in DOM.- Selection-prune watcher (SecretList.vue:939-945) — kritieke correctheid voor bulk-delete/move.
bulkDialogdisabled tijdensloading || folderSwitching— voorkomt bulk-actions tijdens "oude selectie → nieuwe rijen" race.detailRoute.jsLIST_ROUTE_NAMESguard — Alleen de twee list-routes hosten de sidebar;ApplicationDetailetc. triggeren niet.- Dev-only auto-unlock block genuinely gone (commit
b5d5cbfc) — verified via grep: geenNODE_ENV/localStorageopt-in / auto-unlock rest.
🟡 Concerns (4 — allemaal advies, geen blockers)
Zie inline op SecretDetailSidebar.vue regels 1264, 1325, 1385, 1468. Alle vier zijn error-handling / robustness follow-ups — niets blokkerends voor deze restyle.
🟢 Kleine follow-ups (niet inline)
- Component grootte — SecretDetailSidebar.vue is 1748 regels. Ondanks Proton-style "one pane" ontwerp zou een split in
CardRows/IdentityRows/AdvancedAccordionde test-oppervlakte kleiner maken. Trade-off is gedocumenteerd in de comment blocks — niet blocking, wel iets voor Stage 9. - Icon-imports — 30+ vue-material-design-icons in één file. Tree-shakeable en klein, maar allemaal geladen bij sidebar mount. Overweeg
defineAsyncComponentvoor icons in de Advanced-accordion (History, Autorenew, BeehiveOutline). Marginal. - Missing spec-files voor
detailRoute.js(3 pure route-mappers) ensecretTypes.js(1 one-liner) — low value maar even.
CI
mergeStateStatus: UNKNOWN op moment van review — verwacht dat resolveert zodra Actions klaar zijn. Wachten met merge tot CI groen is.
🤖 Generated with Claude Code
| // Backend serializes ownerType / ownerId on the Secret entity; | ||
| // fallback to userId for legacy responses. | ||
| const owner = | ||
| this.secret.ownerId ?? this.secret.owner_id ?? this.secret.userId |
There was a problem hiding this comment.
🟡 isOwner valt terug op drie mogelijke veldnamen — normaliseer aan de backend-kant
const owner = this.secret.ownerId ?? this.secret.owner_id ?? this.secret.userIdDe docblock zegt "fallback to userId for legacy responses", wat op zich prima is — maar drie veldnamen voor hetzelfde concept betekent dat elke andere consumer van Secret óf dezelfde fallback-chain moet dupliceren, óf per abuis één van de drie mist en dan silently isOwner === false voor rechtmatige eigenaars retourneert (fail-closed, dus niet direct exploitable, wél verwarrend).
Verification: grep -rn 'ownerId\|owner_id\|userId' src/ | grep -v '\.spec\.' — als andere consumers alleen ownerId of owner_id checken, hebben zij een impliciete andere definitie van "owner".
Fix (follow-up ticket): normaliseer de payload aan de backend-kant naar één veldnaam (waarschijnlijk ownerId) en verwijder de fallback-chain. Blijft acceptabel als tussenoplossing tot dat gebeurt — 🟡 dus niet blocking.
| * @spec openspec/specs/secrets/spec.md#requirement-read-secret | ||
| */ | ||
| async mounted() { | ||
| await useSecretTypeStore().fetchTypes() |
There was a problem hiding this comment.
🟡 mounted() heeft geen catch rond fetchTypes() — als de type-store faalt blijft de sidebar oneindig loaden
async mounted() {
await useSecretTypeStore().fetchTypes()
await this.load()
}Als fetchTypes() rejecten (netwerk, 500, offline), bubbelt de exception op uit de handler, load() wordt nooit aangeroepen, en this.loading blijft true — NcAppSidebar toont een oneindige spinner zonder foutmelding.
load() zelf heeft wél een try/catch die de error rendert (lines 1341-1370) — voor consistency zou fetchTypes() óók omhuld moeten worden.
Verification: grep -A2 'async mounted' src/components/SecretDetailSidebar.vue — geen try/catch rond het pre-load await.
Fix: omhul beide awaits in dezelfde try/catch, of wrap alleen fetchTypes() en zet de error op this.error bij mislukking.
Fout-scenario: user opent de eerste secret na een deployment waar de types-endpoint tijdelijk 502 gaf → sidebar loopt vast op de spinner tot page refresh.
| */ | ||
| refreshList() { | ||
| useSecretStore() | ||
| .fetchSecrets() |
There was a problem hiding this comment.
🟡 refreshList() slikt errors silently — user krijgt stale list na edit/move zonder signaal
refreshList() {
useSecretStore()
.fetchSecrets()
.catch(() => {})
}Als de list-refresh faalt (offline, auth-timeout, 500) krijgt de user géén signaal, en de list achter de sidebar toont stale data na een edit/move. In combinatie met de sidebar die net wel een verse fetch deed, is de list uit sync met wat de user zojuist zag als "opgeslagen".
Verification: grep -B1 -A3 'refreshList' src/components/SecretDetailSidebar.vue — de .catch(() => {}) is een expliciet silent-swallow.
Fix: minimaal een useOfflineStore().markStale() bij failure, of een logger.warn(...) zodat het te tracken is. Beste: throw en laat de globale error-handler een toast tonen.
Fout-scenario: user edit een secret in de sidebar, save success, refreshList() faalt (WLAN glitch), de list toont nog de oude naam. User denkt "mijn save is niet doorgekomen" en klikt Save opnieuw met de al-opgeslagen waarde.
| * @spec openspec/specs/secrets/spec.md#requirement-delete-secret | ||
| */ | ||
| async remove() { | ||
| await useSecretStore().deleteSecret(this.secretId) |
There was a problem hiding this comment.
🟡 remove() heeft geen error-handling — bij 403/500/offline sluit de sidebar niet, geen feedback
async remove() {
await useSecretStore().deleteSecret(this.secretId)
this.$emit('close')
}Bij een 403 (RBAC), 500 (server error) of offline-write, faalt de await, de exception bubbelt op uit de handler, en $emit('close') wordt nooit uitgevoerd — de sidebar blijft open zonder inline feedback dat de delete faalde. Vergelijk met load() (lines 1341-1370) die wél try/catch heeft en this.error zet.
Verification: grep -A5 'async remove' src/components/SecretDetailSidebar.vue — geen try/catch.
Fix:
async remove() {
try {
await useSecretStore().deleteSecret(this.secretId)
this.$emit('close')
} catch (e) {
this.error = e?.response?.data?.message || t('keepiq', 'Failed to delete secret')
}
}Fout-scenario: user klikt Delete op een gedelegeerde secret waarop hij geen owner-recht heeft (403) → server weigert → sidebar hangt open met de secret nog aanwezig, geen indicatie dat de delete niet werkte.
Four review conversations on the merged PR #479, all in SecretDetailSidebar: - isOwner reads the single canonical `ownerId` field; the owner_id/userId fallback chain was dead code (the Secret entity has serialized ownerId since its first version) and the pinning test now asserts fail-closed behaviour for legacy-style payloads instead. - mounted() catches a failing fetchTypes() and surfaces the error instead of stranding the sidebar on an infinite spinner. - refreshList() no longer swallows a failed list refresh silently; it shows an error toast so a stale list is signalled. - remove() catches a refused delete (403/500/offline) and shows the reason inline instead of leaving the sidebar open without feedback.
The manifest's first-visit tour was translated only in Dutch; the other 36 catalogues missed 12 of its 13 strings and the three menu labels it points at (Features & roadmap, My activity, Flows). Add translations to every catalogue, embedding each language's own menu-label wording so the tour text matches the rendered nav — the Dutch tour is fixed on the same point (it quoted the English labels). Correct wrong-language entries for the referenced labels (Czech/Slovak/Slovenian/Macedonian carried Croatian; Belarusian/Ukrainian carried Russian; Catalan Spanish; Swedish Danish; Luxembourgish German; Romansh Italian), add entries for the two strings introduced by the PR #479 sidebar fixes, and regenerate the .js catalogues (npm run l10n:build).
The Stage-9 catalog and dialog strings in en + all 36 required locales, with the .js browser catalogs regenerated (npm run l10n:build): - the picker labels routed through CnIconColorPicker's translate prop (Color, Icon, Search icons, Default), the 12 color names and the 42 icon names (each language reuses its existing vault/menu terminology); - the vault actions menu and dialogs: Edit/Share/Delete vault, Move vault contents, the contents-move explainer, empty-vault lines, the Target vault label and the honest partial-failure error, plus the empty-delete confirmations; - the two PR #479 sidebar strings that were still missing from the earlier catalog commit (Could not refresh the list, Failed to delete secret). The earlier "Move vault"/"Move folder"/re-parent hint strings are retired but kept in the catalogs, matching how other superseded strings are handled. Extraction and parity gates green.
feat(vault): Proton-style vault icons and colors, vault actions, and post-#479 fixes
Restyle Stage 8 for the vault list, in three arcs:
Secret details in a right sidebar (
edf85380+ l10n follow-ups): clicking a row opens the detail as an NcAppSidebar over the list instead of navigating away — Proton Pass/Passwork style, one scrolling pane, the action row in the header./secrets/:idstays deep-linkable: the id is an optional segment on the list routes, so the list never remounts when a row opens. Folder switches show a loading state instead of flashing the previous folder's rows (0f69672d).Toolbar and selection rework (
20c5f901): the page's three action surfaces collapse into the actions bar — Refresh via the bar's built-in entry, the create/import and "My data" actions in the Actions overflow, the secret-type filter as a funnel button beside the search field (now also carrying the sort radios, and flipping to a filled primary glyph whenever filter or sort deviates from the defaults). The page title is visually hidden (h1 kept for accessibility) and the bar takes the freed top row, clearing the floating nav toggle. Selection is CnIndexPage's own in all three views — header select-all, row/card checkboxes, shift-range preserved through NcCheckboxRadioSwitch — shared across view switches, pruned to visible rows on every list change, with bulk actions living in the library's contextual selection strip (live count viarole="status", WCAG 2.1 SC 4.1.3; bulk-actions §3.1's required bar). Vaults/folders render as a captioned strip above the collection in every view mode instead of masquerading as secrets among rows and cards. New l10n string "Filter and sort" in en + all 36 required locales; six English-copied strings and the secret-type labels got real translations across all locales (17988a77,2e88165d,c12eb9d7).Navigation fixes: the rail's "…" node — not a deep folder's visible ancestor — carries the selection whenever the open folder lives below the 5-level display cap (
58082c6b, generalized ine2836c78).Also:
b5d5cbfcremoves a dev-only auto-unlock block that accidentally rode along in the l10n commit (NODE_ENV-guarded and inert without an explicit localStorage opt-in, so it never shipped in a production build — removed from tracked sources regardless), and41ca063aredraws the lock shackle as a filled outline in all five app icons so ImageMagick's stroke-less favicon rasterizer renders the full lock.Review note: the toolbar-fold, funnel placement, selection strip and vault-strip decisions are recorded with their rationale in the Stage 8 sections of
RESTYLE-PLAN.md(untracked working doc) and inline where each choice lives in the code.