@conduction/nextcloud-vue 2.56.0-beta.8 → 2.56.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +128 -0
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  4. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  5. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  6. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  7. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  9. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  11. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +40 -15
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -2
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  22. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  23. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  24. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  25. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  31. package/dist/esm/l10n/en.json.js +5 -0
  32. package/dist/esm/l10n/en.json.js.map +1 -1
  33. package/dist/esm/l10n/nl.json.js +5 -0
  34. package/dist/esm/l10n/nl.json.js.map +1 -1
  35. package/dist/esm/nextcloud-vue.css +108 -9
  36. package/dist/esm/utils/validateManifestV2.compiled.js +1 -1
  37. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  38. package/dist/nextcloud-vue.cjs.js +857 -490
  39. package/dist/nextcloud-vue.cjs.js.map +2 -2
  40. package/dist/nextcloud-vue.css +108 -9
  41. package/l10n/en.json +5 -0
  42. package/l10n/nl.json +5 -0
  43. package/package.json +3 -2
  44. package/scripts/manifest-schema-hash.js +41 -0
  45. package/scripts/record-manifest-schema-hash.js +52 -0
  46. package/src/components/CnAppNav/CnAppNav.vue +128 -0
  47. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  48. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  49. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  50. package/src/components/CnDashboardPage/CnDashboardPage.vue +30 -6
  51. package/src/components/CnDataTable/CnDataTable.md +1 -1
  52. package/src/components/CnDataTable/CnDataTable.vue +42 -17
  53. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  54. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  55. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  56. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  57. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  58. package/src/css/table.css +1 -2
  59. package/src/public/index.js +5 -0
  60. package/src/schemas/app-manifest-v2.schema.json +1 -1
  61. package/src/utils/validateManifestV2.compiled.js +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CnAppNav.vue3.js","sources":["../../../../src/components/CnAppNav/CnAppNav.vue?vue&type=template&id=2b18f46f&lang.js"],"sourcesContent":["<!--\n CnAppNav — manifest-driven app navigation.\n\n Renders the manifest's `menu[]` array as a Nextcloud app navigation\n (NcAppNavigation + NcAppNavigationItem). One level of nested\n `children[]` is supported. Items are sorted by `order` at BOTH levels —\n top-level entries and a group's children — and items without an order\n render last. Items with a `permission` are filtered against\n the `permissions` prop — when the prop is omitted, all items render.\n\n An optional primary action renders above the main list as an\n NcAppNavigationNew button (e.g. a \"new\" button or an active-context\n switcher). Hosts supply it via the `#primary-action` slot (full\n control over dynamic content + click handling); a static\n `nav.primaryAction` ({ label, icon?, route?, href? }) manifest field\n is the declarative fallback. The slot wins when both are present;\n nothing renders when neither is.\n\n Items split into three RENDERED groups by `section`, plus one section\n this component deliberately renders nowhere:\n - `section: \"main\"` (default) — top of the navigation, scrollable.\n - `section: \"footer\"` — rendered in NcAppNavigation's `#footer` slot,\n OUTSIDE the scrollable list and directly above the settings foldout,\n so they stay visible regardless of how long the main menu is. (The\n earlier `pinned`-prop approach kept them inside the scroll container:\n `margin-top: auto` only bottom-pins while the list does not overflow,\n so apps with long menus showed them mid-scroll.) For always-visible,\n non-settings links: Documentation, Features & Roadmap, About.\n - `section: \"settings\"` — rendered INSIDE an NcAppNavigationSettings\n foldout (the NC-native gear-icon button that slides a panel open).\n A \"Personal settings\" entry is auto-prepended at the top of the\n foldout (opens the host app's NcAppSettingsDialog via\n cnOpenUserSettings); opt out with `nav.includePersonalSettings:\n false`. Right below it, an \"Admin settings\" entry is auto-prepended\n for INSTANCE ADMINS only — gated on the `isAdmin` prop (computed by\n CnAppRoot from `getCurrentUser()?.isAdmin`). It is a LINK to\n `/settings/admin/<appId>` that opens in a NEW TAB and carries an\n open-in-new marker (the destination is outside the app — the marker\n says so up front, the new tab keeps the app open), not a modal: per\n ADR-079 app-level\n configuration lives in Nextcloud's own settings framework, which\n authorizes it server-side. `isAdmin` gates VISIBILITY only and is\n never an authorization decision; it is also NOT interchangeable with\n `isOwner`, which means \"owns this app\" rather than \"administers this\n instance\" and keeps its own separate uses. The\n foldout mounts whenever there are settings items OR personal\n settings is enabled — so every app shows a Settings gear with at\n least Personal settings. It is only fully suppressed when there are\n no settings items AND `nav.includePersonalSettings: false`.\n - `section: \"integrations\"` — rendered by this component NOWHERE. Per\n ADR-110 these are the links that LEAVE this app for another one, and\n they belong in the Integrations section at the bottom of the per-user\n settings modal, which CnAppRoot owns. A link to another app cannot be\n the active route and carries no counter, so putting it in the nav\n makes another app's capability read as this app's feature. The one\n deep link that stays in the navigation is the auto-prepended Admin\n settings above — apps never declare that one themselves.\n\n Manifest and translate are injected from CnAppRoot by default but can\n also be passed as props for standalone use without CnAppRoot. Props\n win over inject when both are present.\n\n Items can opt out of routing in favour of a built-in action by\n setting `action` on the manifest entry. Supported keywords:\n `\"user-settings\"` invokes the `cnOpenUserSettings` provide-injected\n by CnAppRoot, which opens the host app's NcAppSettingsDialog modal;\n `\"admin-settings\"` opens `/settings/admin/<appId>` in a new tab (ADR-079);\n `\"replay-walkthrough\"` invokes `cnReplayWalkthrough` (optionally\n with the item's `tourId`) to re-run the product walkthrough from\n the first step (ADR-043). Both `route` and `href` are ignored when\n `action` is set. The injects default to a no-op so CnAppNav stays\n usable standalone (without a CnAppRoot ancestor).\n\n See REQ-JMR-004 of the json-manifest-renderer specification.\n-->\n<template>\n\t<NcAppNavigation :aria-label=\"ariaLabel\" data-testid=\"cn-nav\">\n\t\t<template v-if=\"$slots.search || $slots.brand || resolvedBrand\" #search>\n\t\t\t<!--\n\t\t\t\t@slot brand\n\t\t\t\t@description Replace the brand block at the very top of the\n\t\t\t\tnavigation. Default content: the logo, name and caption from\n\t\t\t\tthe `brand` prop or the manifest's `nav.brand`. Renders\n\t\t\t\tnothing when neither is set.\n\t\t\t\t@binding {object|null} brand The resolved brand (`{ logo, name, caption, alt }`), or null.\n\t\t\t-->\n\t\t\t<slot name=\"brand\" :brand=\"resolvedBrand\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"resolvedBrand\"\n\t\t\t\t\tclass=\"cn-app-nav__brand\"\n\t\t\t\t\tdata-testid=\"cn-nav-brand\">\n\t\t\t\t\t<!-- Decorative beside a name: the name already says whose\n\t\t\t\t\t app this is, and an alt text would say it twice. A logo\n\t\t\t\t\t on its own carries the name as its alt text. -->\n\t\t\t\t\t<img\n\t\t\t\t\t\tv-if=\"resolvedBrand.logo\"\n\t\t\t\t\t\tclass=\"cn-app-nav__brand-logo\"\n\t\t\t\t\t\t:src=\"resolvedBrand.logo\"\n\t\t\t\t\t\t:alt=\"resolvedBrand.name ? '' : resolvedBrand.alt\">\n\t\t\t\t\t<span v-if=\"resolvedBrand.name || resolvedBrand.caption\" class=\"cn-app-nav__brand-text\">\n\t\t\t\t\t\t<strong v-if=\"resolvedBrand.name\" class=\"cn-app-nav__brand-name\">{{ resolvedBrand.name }}</strong>\n\t\t\t\t\t\t<span v-if=\"resolvedBrand.caption\" class=\"cn-app-nav__brand-caption\">{{ resolvedBrand.caption }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</slot>\n\t\t\t<!--\n\t\t\t\t@slot search\n\t\t\t\t@description Forwarded into NcAppNavigation's #search slot.\n\t\t\t\tHosts mount NcAppNavigationSearch here. When unset, no search\n\t\t\t\tinput renders inside the navigation.\n\t\t\t-->\n\t\t\t<slot name=\"search\" />\n\t\t</template>\n\t\t<!-- @slot primary-action Optional primary action rendered above the\n\t\t main list as NcAppNavigation's top region (e.g. an app's \"new\"\n\t\t button or an active-context switcher). When provided, it takes\n\t\t precedence over the manifest-declared primaryAction (page-scoped\n\t\t OR nav root). Omitted entirely when neither the slot nor any\n\t\t resolvable primaryAction is present. -->\n\t\t<slot name=\"primary-action\">\n\t\t\t<!-- A primary action with an href or route is a real link.\n\t\t\t NcAppNavigationNew cannot render one, so this mirrors its\n\t\t\t wrapper and button. -->\n\t\t\t<div\n\t\t\t\tv-if=\"activePrimaryAction && primaryActionLink\"\n\t\t\t\tclass=\"app-navigation-new cn-app-nav__primary-action\"\n\t\t\t\tdata-testid=\"cn-nav-primary-action\">\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\twide\n\t\t\t\t\tv-bind=\"primaryActionLink\"\n\t\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<component :is=\"primaryActionIconComponent\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ resolveLabel(activePrimaryAction) }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<NcAppNavigationNew\n\t\t\t\tv-else-if=\"activePrimaryAction\"\n\t\t\t\t:text=\"resolveLabel(activePrimaryAction)\"\n\t\t\t\tdata-testid=\"cn-nav-primary-action\"\n\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<component :is=\"primaryActionIconComponent\" :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t</NcAppNavigationNew>\n\t\t</slot>\n\t\t<template #list>\n\t\t\t<template v-for=\"item in mainItems\">\n\t\t\t\t<NcAppNavigationCaption\n\t\t\t\t\tv-if=\"isCaption(item)\"\n\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t:data-testid=\"`cn-nav-caption-${item.id}`\" />\n\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\tv-else\n\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t:to=\"itemTo(item)\"\n\t\t\t\t\t:href=\"itemHref(item)\"\n\t\t\t\t\t:icon=\"cssIconClass(item)\"\n\t\t\t\t\t:active=\"isActive(item)\"\n\t\t\t\t\t:pinned=\"Boolean(item.pinned)\"\n\t\t\t\t\t:allowCollapse=\"visibleChildren(item).length > 0\"\n\t\t\t\t\t:open=\"isItemOpen(item)\"\n\t\t\t\t\t:data-testid=\"`cn-nav-entry-${item.id}`\"\n\t\t\t\t\t:data-cn-route=\"item.route\"\n\t\t\t\t\t@update:open=\"setItemOpen(item, $event)\"\n\t\t\t\t\t@click=\"onItemClick(item, $event)\">\n\t\t\t\t\t<template v-if=\"mdiIconComponent(item) || isRichIcon(item) || isRegistryIcon(item) || isUnresolvedIcon(item)\" #icon>\n\t\t\t\t\t\t<component :is=\"mdiIconComponent(item)\" v-if=\"mdiIconComponent(item)\" :size=\"20\" />\n\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(item)\" :size=\"20\" />\n\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"item.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"resolveCount(item)\" #counter>\n\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t:count=\"resolveCount(item)\"\n\t\t\t\t\t\t\t:active=\"isActive(item)\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"hasItemActionsSlot(item)\" #actions>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot `item-${item.id}-actions`\n\t\t\t\t\t\t\t@description Per-item scoped slot whose content lands inside\n\t\t\t\t\t\t\tthe NcAppNavigationItem's #actions slot for the menu entry\n\t\t\t\t\t\t\twith that id. Use it for inline NcActions menus (e.g. an\n\t\t\t\t\t\t\titem-level \"Pin\" button). Scope receives the menu item.\n\t\t\t\t\t\t\t@binding {object} item The menu item descriptor.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot :name=\"`item-${item.id}-actions`\" :item=\"item\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-for=\"child in visibleChildren(item)\"\n\t\t\t\t\t\t:key=\"child.id\"\n\t\t\t\t\t\t:name=\"resolveLabel(child)\"\n\t\t\t\t\t\t:to=\"itemTo(child)\"\n\t\t\t\t\t\t:href=\"itemHref(child)\"\n\t\t\t\t\t\t:icon=\"cssIconClass(child)\"\n\t\t\t\t\t\t:active=\"isActive(child)\"\n\t\t\t\t\t\t:pinned=\"Boolean(child.pinned)\"\n\t\t\t\t\t\t:data-testid=\"`cn-nav-entry-${child.id}`\"\n\t\t\t\t\t\t:data-cn-route=\"child.route\"\n\t\t\t\t\t\t@click=\"onItemClick(child, $event)\">\n\t\t\t\t\t\t<template v-if=\"mdiIconComponent(child) || isRichIcon(child) || isRegistryIcon(child) || isUnresolvedIcon(child)\" #icon>\n\t\t\t\t\t\t\t<component :is=\"mdiIconComponent(child)\" v-if=\"mdiIconComponent(child)\" :size=\"20\" />\n\t\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(child)\" :size=\"20\" />\n\t\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"child.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-if=\"resolveCount(child)\" #counter>\n\t\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t\t:count=\"resolveCount(child)\"\n\t\t\t\t\t\t\t\t:active=\"isActive(child)\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t</NcAppNavigationItem>\n\t\t\t</template>\n\t\t</template>\n\t\t<template v-if=\"footerItems.length > 0 || showSettingsFoldout\" #footer>\n\t\t\t<!-- Footer-section entries (Documentation, Features & Roadmap,\n\t\t\t About) live in NcAppNavigation's #footer slot — OUTSIDE the\n\t\t\t scrollable list — so they stay visible above the settings\n\t\t\t foldout no matter how long the main menu is. The pinned-prop\n\t\t\t approach only bottom-pinned while the list did not overflow.\n\n\t\t\t NcAppNavigationList, not a bare <ul>: its padding, gap and\n\t\t\t hover highlight are scoped to itself, so they survive being\n\t\t\t slotted. The main list's inset comes from NcAppNavigation's\n\t\t\t scope, which slot content does not carry. -->\n\t\t\t<NcAppNavigationList v-if=\"footerItems.length > 0\" class=\"cn-app-nav__footer-list\">\n\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\tv-for=\"item in footerItems\"\n\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t:to=\"itemTo(item)\"\n\t\t\t\t\t:href=\"itemHref(item)\"\n\t\t\t\t\t:icon=\"cssIconClass(item)\"\n\t\t\t\t\t:active=\"isActive(item)\"\n\t\t\t\t\t:data-testid=\"`cn-nav-entry-${item.id}`\"\n\t\t\t\t\t:data-cn-route=\"item.route\"\n\t\t\t\t\t@click=\"onItemClick(item, $event)\">\n\t\t\t\t\t<template v-if=\"mdiIconComponent(item) || isRichIcon(item) || isRegistryIcon(item) || isUnresolvedIcon(item)\" #icon>\n\t\t\t\t\t\t<component :is=\"mdiIconComponent(item)\" v-if=\"mdiIconComponent(item)\" :size=\"20\" />\n\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(item)\" :size=\"20\" />\n\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"item.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"resolveCount(item)\" #counter>\n\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t:count=\"resolveCount(item)\"\n\t\t\t\t\t\t\t:active=\"isActive(item)\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcAppNavigationItem>\n\t\t\t</NcAppNavigationList>\n\t\t\t<!-- Settings foldout (section: \"settings\" items). NC-native\n\t\t\t gear-icon button that slides open a panel; the first entry\n\t\t\t is an auto-prepended \"Personal settings\" that opens the\n\t\t\t host app's NcAppSettingsDialog via cnOpenUserSettings. -->\n\t\t\t<NcAppNavigationSettings\n\t\t\t\tv-if=\"showSettingsFoldout\"\n\t\t\t\t:name=\"settingsFoldoutLabel\"\n\t\t\t\tdata-testid=\"cn-nav-settings\">\n\t\t\t\t<ul class=\"cn-app-nav__settings-list\">\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"includePersonalSettings\"\n\t\t\t\t\t\t:name=\"personalSettingsLabel\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-personal-settings\"\n\t\t\t\t\t\t@click=\"onPersonalSettingsClick\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Cog :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"roadmapEntry\"\n\t\t\t\t\t\t:name=\"roadmapEntry.label\"\n\t\t\t\t\t\t:to=\"roadmapEntry.to\"\n\t\t\t\t\t\t:href=\"roadmapEntry.href\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-roadmap\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<MapMarkerPath :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"documentationEntry\"\n\t\t\t\t\t\t:name=\"documentationEntry.label\"\n\t\t\t\t\t\t:href=\"documentationEntry.href\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-documentation\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<BookOpenVariant :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<!-- The target is Nextcloud's OWN settings area, not an\n\t\t\t\t\t in-app route: a new tab keeps the app open, and the\n\t\t\t\t\t trailing open-in-new marker tells the user they are\n\t\t\t\t\t leaving the app before they click. The new tab comes\n\t\t\t\t\t from the href being ABSOLUTE (see adminSettingsHref) —\n\t\t\t\t\t NcAppNavigationItem ignores a `target` attr and only\n\t\t\t\t\t sets target=\"_blank\" for scheme-prefixed hrefs. -->\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"showAdminSettingsLink\"\n\t\t\t\t\t\t:name=\"adminSettingsLabel\"\n\t\t\t\t\t\t:href=\"adminSettingsHref\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-admin-settings\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<ShieldAccountOutline :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template #counter>\n\t\t\t\t\t\t\t<OpenInNew :size=\"16\" :title=\"opensInNewTabHint\" data-testid=\"cn-nav-admin-settings-external\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<template v-for=\"item in settingsItems\">\n\t\t\t\t\t\t<NcAppNavigationCaption\n\t\t\t\t\t\t\tv-if=\"isCaption(item)\"\n\t\t\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-nav-caption-${item.id}`\" />\n\t\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t\t\t:to=\"itemTo(item)\"\n\t\t\t\t\t\t\t:href=\"itemHref(item)\"\n\t\t\t\t\t\t\t:icon=\"cssIconClass(item)\"\n\t\t\t\t\t\t\t:active=\"isActive(item)\"\n\t\t\t\t\t\t\t:data-cn-route=\"item.route\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-nav-entry-${item.id}`\"\n\t\t\t\t\t\t\t@click=\"onItemClick(item, $event)\">\n\t\t\t\t\t\t\t<template v-if=\"mdiIconComponent(item) || isRichIcon(item) || isRegistryIcon(item) || isUnresolvedIcon(item)\" #icon>\n\t\t\t\t\t\t\t\t<component :is=\"mdiIconComponent(item)\" v-if=\"mdiIconComponent(item)\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(item)\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"item.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t<template v-if=\"resolveCount(item)\" #counter>\n\t\t\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t\t\t:count=\"resolveCount(item)\"\n\t\t\t\t\t\t\t\t\t:active=\"isActive(item)\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t</template>\n\t\t\t\t</ul>\n\t\t\t</NcAppNavigationSettings>\n\t\t</template>\n\t</NcAppNavigation>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcAppNavigation, NcAppNavigationCaption, NcAppNavigationItem, NcAppNavigationList, NcAppNavigationNew, NcAppNavigationSettings, NcButton, NcCounterBubble } from '@nextcloud/vue'\nimport BookOpenVariant from 'vue-material-design-icons/BookOpenVariant.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\n// ADR-077 rule 4: visible fallback for an unresolvable icon name.\nimport HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'\nimport MapMarkerPath from 'vue-material-design-icons/MapMarkerPath.vue'\nimport OpenInNew from 'vue-material-design-icons/OpenInNew.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport ShieldAccountOutline from 'vue-material-design-icons/ShieldAccountOutline.vue'\nimport { ICON_MAP } from '../CnIcon/CnIcon.vue'\nimport CnMenuItemIcon from '../CnMenuWidget/CnMenuItemIcon.vue'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { isSvgPath } from '../../utils/iconUtils.js'\nimport { passesContextPredicates } from '../../utils/visibleIfContext.js'\n// The legacy `icon-*` → MDI bridge lives beside CnIcon now, so the menu EDITOR\n// resolves a seeded `icon-comment` the same way this nav does. It used to be a\n// map local to this file, which is why the nav drew a proper glyph and the\n// editor drew a help-circle \"?\" for the very same manifest value.\nimport { bridgedMdiForCssIcon } from '../CnIcon/cssIconBridge.js'\nimport { getSemanticIconComponent } from '../CnIcon/semanticIcons.js'\nimport { hasRegistryIcon, isCustomIconUrl } from '../CnWidgetGrid/widgetIcons.js'\n\n/**\n * Order two menu entries by their manifest `order`.\n *\n * Entries carrying an `order` come first, ascending; entries without one render\n * last and keep their relative order. Used at BOTH nav levels so a group's\n * children obey the same rule as the top-level list.\n *\n * @param {object} a First entry.\n * @param {object} b Second entry.\n * @return {number} Comparator result.\n */\nfunction byManifestOrder(a, b) {\n\tconst aHas = typeof a.order === 'number'\n\tconst bHas = typeof b.order === 'number'\n\tif (aHas && !bHas) {\n\t\treturn -1\n\t}\n\tif (!aHas && bHas) {\n\t\treturn 1\n\t}\n\tif (!aHas && !bHas) {\n\t\treturn 0\n\t}\n\treturn a.order - b.order\n}\n\nexport default {\n\tname: 'CnAppNav',\n\n\tcomponents: {\n\t\tNcAppNavigation,\n\t\tNcAppNavigationCaption,\n\t\tNcAppNavigationItem,\n\t\tNcAppNavigationList,\n\t\tNcAppNavigationNew,\n\t\tNcAppNavigationSettings,\n\t\tNcButton,\n\t\tNcCounterBubble,\n\t\tCnMenuItemIcon,\n\t\tCog,\n\t\tHelpCircleOutline,\n\t\tShieldAccountOutline,\n\t\tMapMarkerPath,\n\t\tBookOpenVariant,\n\t\tOpenInNew,\n\t},\n\n\tinject: {\n\t\tcnManifest: { default: null },\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * Provided by CnAppRoot — opens the host app's\n\t\t * NcAppSettingsDialog. Defaults to a no-op so CnAppNav is\n\t\t * still usable when mounted outside a CnAppRoot ancestor;\n\t\t * the click silently does nothing in that case rather than\n\t\t * throwing.\n\t\t */\n\t\tcnOpenUserSettings: { default: () => () => {} },\n\t\t/**\n\t\t * Provided by CnAppRoot — the consuming app's slug, used to build\n\t\t * the \"Admin settings\" link target `/settings/admin/<appId>`.\n\t\t * Overridden by the explicit `appId` prop when both are present.\n\t\t * Null outside a CnAppRoot ancestor, in which case the link is\n\t\t * suppressed (ADR-079 §2).\n\t\t */\n\t\tcnAppId: { default: null },\n\t\t/**\n\t\t * Provided by CnAppRoot — restarts the product walkthrough (ADR-043)\n\t\t * from the first step. Bound to menu entries declaring\n\t\t * `action: \"replay-walkthrough\"` (optionally with a `tourId`).\n\t\t * Defaults to a no-op so CnAppNav stays usable standalone.\n\t\t */\n\t\tcnReplayWalkthrough: { default: () => () => {} },\n\t\t/**\n\t\t * Provided by CnAppRoot — reactive `{ [register]: { [schema]: number } }`\n\t\t * map populated from `useObjectStore().totals` for every\n\t\t * `count: \"auto\"` menu entry whose resolved page is `type: \"index\"`\n\t\t * with `register + schema` in its config. Defaults to an empty\n\t\t * object so `resolveCount` returns `null` (no badge) when CnAppNav\n\t\t * is mounted outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {{ [register: string]: { [schema: string]: number } }}\n\t\t */\n\t\tcnMenuCounts: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Manifest object. Falls back to injected `cnManifest`. Provide\n\t\t * explicitly when mounting CnAppNav outside of CnAppRoot.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tmanifest: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`,\n\t\t * which itself defaults to an identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * List of permission strings the current user holds. Items\n\t\t * declaring a `permission` only render when their permission\n\t\t * appears in this list. When the prop is omitted (or empty),\n\t\t * all items are visible regardless of their permission field.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tpermissions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user is an OWNER of this app\n\t\t * (admin-settings-owner-gating capability). Computed by CnAppRoot\n\t\t * from `currentUserGroups` ∩ `permissions.owners` and/or a manifest\n\t\t * `runtime.user` owner signal — deliberately NOT `OC.isUserAdmin()`.\n\t\t * Read by nothing in this component: the auto-included \"Admin\n\t\t * settings\" entry is gated on `isAdmin` alone (see the file header\n\t\t * and `showAdminSettingsLink`). The prop stays because CnAppRoot\n\t\t * binds it as published API and owner-gating is its own question,\n\t\t * not the instance-admin one.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tisOwner: { // eslint-disable-line vue/no-unused-properties -- published prop bound by CnAppRoot; owner-gating is decided upstream, not here\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user administers this Nextcloud instance.\n\t\t * Computed by CnAppRoot from `getCurrentUser()?.isAdmin`\n\t\t * (`@nextcloud/auth`) — NOT the legacy `OC.isUserAdmin()` global,\n\t\t * and NOT interchangeable with `isOwner`, which is about owning a\n\t\t * given app rather than administering the instance (ADR-079 §3).\n\t\t *\n\t\t * Gates VISIBILITY of the \"Admin settings\" link only. It is not an\n\t\t * authorization decision and MUST NOT be used as one: the boundary\n\t\t * is Nextcloud's settings framework, which refuses\n\t\t * `/settings/admin/<app>` server-side for non-admins. Defaults to\n\t\t * `false` so CnAppNav mounted standalone never shows the link.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tisAdmin: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * App id used to build the \"Admin settings\" link target\n\t\t * (`/settings/admin/<appId>`). Falls back to the injected\n\t\t * `cnAppId` provided by CnAppRoot. With neither available the\n\t\t * link is suppressed rather than pointing at a broken URL.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tappId: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the navigation landmark, forwarded to\n\t\t * `NcAppNavigation`'s `aria-label`.\n\t\t *\n\t\t * `@nextcloud/vue` 9 requires `ariaLabel` or `ariaLabelledby` and warns\n\t\t * when neither is set: a `<nav>` with no accessible name is announced as\n\t\t * an unlabelled landmark, so a screen-reader user tabbing the landmark\n\t\t * list cannot tell it from any other nav on the page (WCAG 2.4.6 /\n\t\t * 1.3.1). The default covers every consumer without a code change; pass\n\t\t * your own when an app shows more than one navigation, since the point\n\t\t * of the name is to tell them apart.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Main navigation'),\n\t\t},\n\n\t\t/**\n\t\t * The brand block at the top of the navigation:\n\t\t * `{ logo?, name?, caption?, alt? }`. `logo` is an image URL, `name`\n\t\t * the app or organisation name, `caption` a line under it. Falls back\n\t\t * to the manifest's `nav.brand`. `null` (the default) with no manifest\n\t\t * brand renders nothing.\n\t\t *\n\t\t * @type {{logo?: string, name?: string, caption?: string, alt?: string}|null}\n\t\t */\n\t\tbrand: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['primary-action', 'primary-action-click'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * Per-item expand/collapse state for menu groups, keyed by\n\t\t\t * item id. Seeded lazily from the manifest's `item.open` by\n\t\t\t * `isItemOpen`; written by the collapse chevron (via\n\t\t\t * `@update:open`), by title clicks on route-less group items\n\t\t\t * (which TOGGLE open/closed), and by title clicks on a group\n\t\t\t * that ALSO carries a route or href (which force it OPEN,\n\t\t\t * alongside the navigation — see `onItemClick`).\n\t\t\t */\n\t\t\topenState: {},\n\n\t\t}\n\t},\n\n\tcomputed: {\n\t\teffectiveManifest() {\n\t\t\treturn this.manifest ?? this.cnManifest\n\t\t},\n\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The brand to draw: the `brand` prop, else the manifest's\n\t\t * `nav.brand`. Name and caption go through the translate function,\n\t\t * like every other manifest label. Null when there is nothing to\n\t\t * show, which is what keeps the block out of a navigation that\n\t\t * declares no brand.\n\t\t *\n\t\t * @return {{logo: string, name: string, caption: string, alt: string}|null} The brand, or null.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-navigation-brand\n\t\t */\n\t\tresolvedBrand() {\n\t\t\tconst declared = this.brand ?? this.effectiveManifest?.nav?.brand\n\t\t\tif (!declared || typeof declared !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst text = (value) => (typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : '')\n\t\t\tconst brand = {\n\t\t\t\tlogo: typeof declared.logo === 'string' ? declared.logo : '',\n\t\t\t\tname: text(declared.name),\n\t\t\t\tcaption: text(declared.caption),\n\t\t\t\talt: text(declared.alt),\n\t\t\t}\n\t\t\treturn (brand.logo || brand.name || brand.caption) ? brand : null\n\t\t},\n\n\t\t/**\n\t\t * Manifest-declared root-level primary action (`nav.primaryAction`)\n\t\t * — retained for backwards compatibility with code that reads this\n\t\t * computed. New code SHOULD read `activePrimaryAction` instead,\n\t\t * which also handles the page-scoped override.\n\t\t *\n\t\t * @return {object|null} `{ label, icon?, route?, href?, id?, payload? }` or null.\n\t\t */\n\t\tprimaryAction() {\n\t\t\treturn this.effectiveManifest?.nav?.primaryAction ?? null\n\t\t},\n\n\t\t/**\n\t\t * Resolved primary action for the current route. Resolution order:\n\t\t * 1. The current route's matching `pages[].primaryAction`\n\t\t * 2. `manifest.nav.primaryAction` as app-wide default\n\t\t * 3. `null` — no primary-action button renders\n\t\t *\n\t\t * Page-scoped declarations always win over the nav-root default.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactivePrimaryAction() {\n\t\t\tconst pages = this.effectiveManifest?.pages ?? []\n\t\t\tconst routeName = this.$route?.name\n\t\t\tif (routeName) {\n\t\t\t\tconst page = pages.find((p) => p.id === routeName)\n\t\t\t\tif (page && page.primaryAction) {\n\t\t\t\t\treturn page.primaryAction\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.effectiveManifest?.nav?.primaryAction ?? null\n\t\t},\n\n\t\t/**\n\t\t * MDI icon component for the resolved primary action. Honors the\n\t\t * action's `icon` field when set; falls back to the canonical\n\t\t * `Plus` icon to match NC's NcAppNavigationNew default styling.\n\t\t *\n\t\t * @return {import('vue').Component}\n\t\t */\n\t\tprimaryActionIconComponent() {\n\t\t\treturn this.mdiIconComponent(this.activePrimaryAction) ?? Plus\n\t\t},\n\n\t\t/**\n\t\t * Link props for the primary action's button: an `href` opens in a\n\t\t * new tab, a `route` is a router link. Null when the action is\n\t\t * neither (or there is no router), so it stays a plain button.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tprimaryActionLink() {\n\t\t\tconst action = this.activePrimaryAction\n\t\t\tif (!action) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (action.href) {\n\t\t\t\treturn { href: action.href, target: '_blank' }\n\t\t\t}\n\t\t\tif (action.route && this.$router) {\n\t\t\t\treturn { to: { name: action.route } }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * All visible items (filtered by permission and visibleIf conditions,\n\t\t * sorted by order). Retained for backwards-compat with the previous\n\t\t * public API and tests that read this computed; new code should use\n\t\t * `mainItems` / `settingsItems` instead.\n\t\t */\n\t\tvisibleItems() {\n\t\t\treturn (this.effectiveManifest?.menu ?? [])\n\t\t\t\t.filter((item) => this.passesPermission(item) && this.passesVisibleIf(item))\n\t\t\t\t.slice()\n\t\t\t\t.sort(byManifestOrder)\n\t\t},\n\n\t\t/**\n\t\t * Items that render in the top list (default placement).\n\t\t *\n\t\t * Note the `=== 'main'` rather than a negated list: an entry whose\n\t\t * section this component does not render must fall through to\n\t\t * NOTHING, not to the main list. `section: \"integrations\"` is\n\t\t * exactly that case — those entries render in CnAppRoot's\n\t\t * per-user settings modal, not in the navigation (ADR-110) — and a\n\t\t * `!== 'footer' && !== 'settings'` test would have quietly put\n\t\t * every one of them back in the nav this contract removes them\n\t\t * from.\n\t\t */\n\t\tmainItems() {\n\t\t\treturn this.visibleItems.filter((item) => (item.section ?? 'main') === 'main')\n\t\t},\n\n\t\t/**\n\t\t * Items pinned to the bottom of the navigation (section:\n\t\t * \"footer\") — rendered as flat NcAppNavigationItems above the\n\t\t * settings foldout. For always-visible, non-settings entries:\n\t\t * Documentation, Features & Roadmap, About.\n\t\t */\n\t\tfooterItems() {\n\t\t\treturn this.visibleItems.filter((item) => item.section === 'footer')\n\t\t},\n\n\t\t/**\n\t\t * Items that render INSIDE the NcAppNavigationSettings foldout\n\t\t * (section: \"settings\"). The foldout is the NC-native gear-icon\n\t\t * button that slides a panel open; these entries are app-level\n\t\t * configuration pages (Forms, Pipelines, Automations, …).\n\t\t */\n\t\tsettingsItems() {\n\t\t\treturn this.visibleItems.filter((item) => item.section === 'settings')\n\t\t},\n\n\t\t/**\n\t\t * Whether the settings foldout mounts. Mounts when there is at\n\t\t * least one `section: \"settings\"` item OR personal settings is\n\t\t * enabled — so every app shows a Settings gear with at least the\n\t\t * auto-prepended \"Personal settings\" entry. Only fully suppressed\n\t\t * when there are no settings items AND `nav.includePersonalSettings`\n\t\t * is explicitly `false`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowSettingsFoldout() {\n\t\t\treturn this.settingsItems.length > 0 || this.includePersonalSettings\n\t\t\t\t|| this.roadmapEntry !== null || this.documentationEntry !== null\n\t\t},\n\n\t\t/**\n\t\t * Whether to auto-prepend the \"Personal settings\" entry at the top\n\t\t * of the foldout. On by default; opt out with\n\t\t * `manifest.nav.includePersonalSettings: false` (e.g. when the app\n\t\t * has no per-user NcAppSettingsDialog wired through\n\t\t * cnOpenUserSettings).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tincludePersonalSettings() {\n\t\t\treturn this.effectiveManifest?.nav?.includePersonalSettings !== false\n\t\t},\n\n\t\t/**\n\t\t * Label for the foldout's gear button. Manifest override:\n\t\t * `nav.settingsLabel`; defaults to \"Settings\".\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsettingsFoldoutLabel() {\n\t\t\tconst custom = this.effectiveManifest?.nav?.settingsLabel\n\t\t\tif (typeof custom === 'string' && custom.length > 0) {\n\t\t\t\treturn this.effectiveTranslate(custom)\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Settings')\n\t\t},\n\n\t\t/**\n\t\t * Label for the auto-prepended Personal-settings entry.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tpersonalSettingsLabel() {\n\t\t\treturn t('nextcloud-vue', 'Personal settings')\n\t\t},\n\n\t\t/**\n\t\t * Optional \"Features & roadmap\" foldout entry. Enabled via\n\t\t * `nav.includeRoadmap`; `nav.roadmapUrl` is treated as an external link\n\t\t * when it looks like a URL, otherwise as an in-app router target.\n\t\t *\n\t\t * @return {{label: string, to: (string|null), href: (string|null)}|null}\n\t\t */\n\t\troadmapEntry() {\n\t\t\tconst nav = this.effectiveManifest?.nav\n\t\t\tif (!nav || nav.includeRoadmap !== true) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = (typeof nav.roadmapLabel === 'string' && nav.roadmapLabel)\n\t\t\t\t? this.effectiveTranslate(nav.roadmapLabel)\n\t\t\t\t: t('nextcloud-vue', 'Features & roadmap')\n\t\t\tconst target = typeof nav.roadmapUrl === 'string' ? nav.roadmapUrl.trim() : ''\n\t\t\tconst external = /^(https?:)?\\/\\//.test(target)\n\t\t\treturn { label, to: (target && !external) ? target : null, href: external ? target : null }\n\t\t},\n\n\t\t/**\n\t\t * Optional \"Documentation\" foldout entry. Enabled via\n\t\t * `nav.includeDocumentation`; always an external link (`nav.documentationUrl`).\n\t\t *\n\t\t * @return {{label: string, href: string}|null}\n\t\t */\n\t\tdocumentationEntry() {\n\t\t\tconst nav = this.effectiveManifest?.nav\n\t\t\tif (!nav || nav.includeDocumentation !== true) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst target = typeof nav.documentationUrl === 'string' ? nav.documentationUrl.trim() : ''\n\t\t\tif (!target) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = (typeof nav.documentationLabel === 'string' && nav.documentationLabel)\n\t\t\t\t? this.effectiveTranslate(nav.documentationLabel)\n\t\t\t\t: t('nextcloud-vue', 'Documentation')\n\t\t\treturn { label, href: target }\n\t\t},\n\n\t\t/**\n\t\t * Label for the auto-prepended Admin-settings entry.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tadminSettingsLabel() {\n\t\t\treturn t('nextcloud-vue', 'Admin settings')\n\t\t},\n\n\t\t/**\n\t\t * Accessible name (and hover tooltip) of the open-in-new marker on\n\t\t * the Admin-settings entry — the visual cue that the link leaves\n\t\t * the app for Nextcloud's own settings area.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\topensInNewTabHint() {\n\t\t\treturn t('nextcloud-vue', 'Opens in a new tab')\n\t\t},\n\n\t\t/**\n\t\t * Resolved app id for the Admin-settings link — the explicit\n\t\t * `appId` prop, else the `cnAppId` provided by CnAppRoot.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\teffectiveAppId() {\n\t\t\tconst id = this.appId || this.cnAppId\n\t\t\treturn (typeof id === 'string' && id.trim()) ? id.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * Target of the Admin-settings entry: the app's section in\n\t\t * Nextcloud's own admin settings (ADR-079 §1). This is a LINK, not\n\t\t * a modal — the app never re-implements the settings surface, and\n\t\t * the destination is authorized server-side by the settings\n\t\t * framework.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tadminSettingsHref() {\n\t\t\tif (!this.effectiveAppId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\t// ABSOLUTE (same-origin) on purpose: NcAppNavigationItem renders\n\t\t\t// target=\"_blank\" only for hrefs its isExternal() deems external\n\t\t\t// (scheme-prefixed) and ignores a passed `target` — a relative\n\t\t\t// path therefore always opens in the SAME tab. The absolute form\n\t\t\t// gets the new tab while keeping native anchor semantics\n\t\t\t// (middle-click, copy link).\n\t\t\tconst path = generateUrl('/settings/admin/{appId}', { appId: this.effectiveAppId })\n\t\t\tconst origin = (typeof window !== 'undefined' && window.location && window.location.origin) || ''\n\t\t\treturn origin + path\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the Admin-settings link: the user administers the\n\t\t * instance AND we can build a target. Visibility only — see the\n\t\t * `isAdmin` prop docs; the access check lives in Nextcloud.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowAdminSettingsLink() {\n\t\t\treturn this.isAdmin === true && this.adminSettingsHref !== ''\n\t\t},\n\n\t\t/**\n\t\t * Route name of the menu item that best matches the current route.\n\t\t * A direct match (current route name IS a menu target) wins;\n\t\t * otherwise the current path is matched against each item's page\n\t\t * path and the LONGEST prefix wins, so detail / nested routes (e.g.\n\t\t * `/expenses/:id`) light up — and auto-expand — their index entry\n\t\t * even though the detail route name is not in the menu. Longest\n\t\t * prefix disambiguates nested namespaces (e.g. `/pos` vs\n\t\t * `/pos/refunds`). Returns null when nothing matches.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tactiveRouteName() {\n\t\t\tconst routeName = this.$route?.name\n\t\t\tconst path = this.$route?.path\n\t\t\tconst flat = []\n\t\t\tfor (const item of this.visibleItems) {\n\t\t\t\tflat.push(item)\n\t\t\t\tfor (const child of this.visibleChildren(item)) {\n\t\t\t\t\tflat.push(child)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (routeName && flat.some((it) => it.route === routeName)) {\n\t\t\t\treturn routeName\n\t\t\t}\n\t\t\tif (!path) {\n\t\t\t\treturn routeName ?? null\n\t\t\t}\n\t\t\tlet best = null\n\t\t\tlet bestLen = -1\n\t\t\tfor (const it of flat) {\n\t\t\t\tif (!it.route) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst pagePath = this.pageForItem(it)?.route\n\t\t\t\tif (!pagePath || pagePath === '/' || pagePath.includes(':')) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (path === pagePath || path.startsWith(pagePath + '/')) {\n\t\t\t\t\tif (pagePath.length > bestLen) {\n\t\t\t\t\t\tbest = it.route\n\t\t\t\t\t\tbestLen = pagePath.length\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn best ?? routeName ?? null\n\t\t},\n\t},\n\n\twatch: {\n\t\tactiveRouteName: {\n\t\t\timmediate: true,\n\t\t\thandler(_current, previous) {\n\t\t\t\tthis.pinGroupsEntered(previous)\n\t\t\t},\n\t\t},\n\n\t\t// The manifest can arrive after the route did.\n\t\tvisibleItems() {\n\t\t\tthis.pinGroupsEntered(this.activeRouteName)\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Non-reactive one-shot latch for warnAutoCountMisconfigured(). It has\n\t\t// to be seeded here rather than lazily on first use: `resolveCount()`\n\t\t// runs DURING RENDER, and Vue 3's instance proxy emits\n\t\t// \"Property \"_autoCountWarned\" was accessed during render but is not\n\t\t// defined on instance\" for the first read of an unset instance field\n\t\t// (Vue 2 read it back as plain `undefined`, silently). It deliberately\n\t\t// stays out of `data()`: a reactive Set mutated inside render would\n\t\t// re-trigger the render effect.\n\t\tthis._autoCountWarned = new Set()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve a menu item's `icon` string to an MDI Vue component. MDI names\n\t\t * resolve via the per-app `registerIcons()` registry; legacy Nextcloud\n\t\t * `icon-*` class names resolve via the shared `cssIconBridge` so they\n\t\t * render monochrome (fill:currentColor) like every other glyph.\n\t\t * Returns `null` (→ the `:icon=\"cssIconClass(item)\"` CSS-class fallback)\n\t\t * for unbridged `icon-*` names and unknown MDI names.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {import('vue').Component|null}\n\t\t */\n\t\tmdiIconComponent(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (icon.startsWith('icon-')) {\n\t\t\t\treturn bridgedMdiForCssIcon(icon) || null\n\t\t\t}\n\t\t\t// ADR-077: the app's own registerIcons() entries win (so an app can\n\t\t\t// override), then the shared semantic vocabulary — which resolves\n\t\t\t// WITHOUT the app having registered anything. Before the vocabulary\n\t\t\t// existed, a name the app forgot to register resolved to null here\n\t\t\t// and the entry rendered with no icon at all.\n\t\t\treturn ICON_MAP[icon] || getSemanticIconComponent(icon) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the item declares an icon that NOTHING can resolve — not the\n\t\t * app registry, not the semantic vocabulary, not the widget registry, and\n\t\t * not a URL / SVG path.\n\t\t *\n\t\t * ADR-077 rule 4: such an icon MUST degrade to a visible fallback glyph.\n\t\t * Historically it rendered as empty space, which is indistinguishable\n\t\t * from a deliberate no-icon design choice — 29 of hrmq's 72 nav entries\n\t\t * were silently blank this way. A visible \"?\" is discoverable; blank is\n\t\t * not.\n\t\t *\n\t\t * `icon-*` values are excluded: those have their own CSS-class fallback\n\t\t * path via the `cssIconClass` method.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {boolean} true when the declared icon resolves to nothing.\n\t\t */\n\t\tisUnresolvedIcon(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (icon.startsWith('icon-')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !this.mdiIconComponent(item)\n\t\t\t\t&& !this.isRichIcon(item)\n\t\t\t\t&& !this.isRegistryIcon(item)\n\t\t},\n\n\t\t/**\n\t\t * Whether the item's icon is a raw SVG path or an image URL (incl. the\n\t\t * `data:` URIs the bundled NL-government sets emit) — neither of which is a\n\t\t * component, so `ICON_MAP` can't resolve it and the `#icon` slot must\n\t\t * render it through CnMenuItemIcon instead.\n\t\t *\n\t\t * Without this, an icon picked from CnIconBrowser's Gemeente / Den Haag /\n\t\t * RVO tabs would simply not appear in the navigation.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {boolean} true for path/URL icons.\n\t\t */\n\t\tisRichIcon(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0 || icon.startsWith('icon-')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn isCustomIconUrl(icon) || isSvgPath(icon)\n\t\t},\n\n\t\t/**\n\t\t * Whether the item's icon is a plain MDI *name* that the shared widget-icon\n\t\t * registry can render (e.g. \"Heart\", \"Home\" — what CnIconBrowser emits from\n\t\t * its component-name sources).\n\t\t *\n\t\t * `ICON_MAP` only holds what the consuming app passed to `registerIcons()`,\n\t\t * and an app rendering USER-AUTHORED manifests cannot pre-register whatever\n\t\t * icon a user might pick — Buildiq registers none at all. So a picked name\n\t\t * failed `mdiIconComponent` (not registered) AND `isRichIcon` (not a URL or\n\t\t * SVG path), the `#icon` slot was skipped entirely, and the menu item rendered\n\t\t * with NO icon — even though CnMenuItemIcon → CnWidgetIcon could resolve it.\n\t\t *\n\t\t * Gate on the registry actually HAVING the name: `getIconComponent` answers\n\t\t * the default icon for anything unknown, so gating on it would render a wrong\n\t\t * (but plausible) icon for a typo instead of none.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {boolean} true when the widget-icon registry can render this name.\n\t\t */\n\t\tisRegistryIcon(item) {\n\t\t\treturn hasRegistryIcon(item?.icon)\n\t\t},\n\n\t\t/**\n\t\t * Pass-through for the `:icon` prop on NcAppNavigationItem when\n\t\t * the manifest declares a Nextcloud CSS-class icon (`icon-*`).\n\t\t * Returns an empty string when the icon is an MDI name OR a bridged\n\t\t * `icon-*` (rendered via the `#icon` slot instead), so NcAppNavigationItem\n\t\t * doesn't also paint a CSS-class background-image.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {string}\n\t\t */\n\t\tcssIconClass(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (!icon.startsWith('icon-')) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn bridgedMdiForCssIcon(icon) ? '' : icon\n\t\t},\n\n\t\tpassesPermission(item) {\n\t\t\tif (!item.permission) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (!this.permissions || this.permissions.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.permissions.includes(item.permission)\n\t\t},\n\n\t\t/**\n\t\t * Evaluate a menu item's `visibleIf` condition block.\n\t\t *\n\t\t * Returns `true` (visible) when:\n\t\t * - No `visibleIf` is declared (backwards-compatible default).\n\t\t * - `visibleIf.appInstalled` is set AND the named app is\n\t\t * installed / enabled (checked via `OC.appswebroots` then the\n\t\t * capabilities fallback, cached per page load by `isAppInstalled`).\n\t\t * - Context-path predicates (any key that is a dot-separated path\n\t\t * into `manifest.runtime`) all pass against the current runtime\n\t\t * data. Predicates are evaluated by `passesContextPredicates`.\n\t\t * Example: `{ \"user.primaryRole\": { \"in\": [\"hr\", \"compliance\"] } }`\n\t\t * hides the entry unless `manifest.runtime.user.primaryRole` is\n\t\t * `\"hr\"` or `\"compliance\"`. When the runtime block is absent the\n\t\t * entry is hidden (fail-safe: never show role-gated items to\n\t\t * unidentified users).\n\t\t *\n\t\t * All conditions are combined with implicit AND — every condition\n\t\t * must pass for the item to render. Returns `false` when any fails.\n\t\t *\n\t\t * @param {object} item Menu item (or child) to evaluate.\n\t\t * @return {boolean} Whether the item should render.\n\t\t */\n\t\tpassesVisibleIf(item) {\n\t\t\tconst condition = item.visibleIf\n\t\t\tif (!condition || typeof condition !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// Specialised condition: appInstalled.\n\t\t\tif (condition.appInstalled) {\n\t\t\t\tif (!isAppInstalled(condition.appInstalled)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Context-path predicates: any non-reserved key is a dot-path\n\t\t\t// into manifest.runtime evaluated by passesContextPredicates.\n\t\t\tconst runtime = this.effectiveManifest?.runtime ?? null\n\t\t\tif (!passesContextPredicates(condition, runtime)) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * A group's visible children, in `order`.\n\t\t *\n\t\t * The SORT is the fix for a silent no-op: this used to filter only, so\n\t\t * `order` on a child was accepted, documented and ignored. It bites\n\t\t * hardest on relocated leaves, because `applyMenuRelocations()` walks\n\t\t * the menu backwards and appends each move, which lands a group's\n\t\t * children in REVERSE manifest order — an order nobody wrote down and\n\t\t * no app can influence except by reordering `manifest.json` itself.\n\t\t * Pipelinq's Customer Support group read \"Projecten, My Work, Queue,\n\t\t * Tasks, All tickets\" against declared orders of 55, 20, 10, 60, 50.\n\t\t *\n\t\t * Same comparator as the top level, so one rule governs both: items\n\t\t * without an `order` render last, in their existing relative order\n\t\t * (Array.prototype.sort is stable).\n\t\t *\n\t\t * @param {object} item The group whose children to render.\n\t\t * @return {Array<object>} Visible children, ordered.\n\t\t */\n\t\tvisibleChildren(item) {\n\t\t\tif (!Array.isArray(item.children)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn item.children\n\t\t\t\t.filter((c) => this.passesPermission(c) && this.passesVisibleIf(c))\n\t\t\t\t.sort(byManifestOrder)\n\t\t},\n\n\t\tresolveLabel(item) {\n\t\t\treturn this.effectiveTranslate(item.label)\n\t\t},\n\n\t\t/**\n\t\t * Whether a menu entry is the page the reader is on.\n\t\t *\n\t\t * Two entries may share a route and differ only in `query` (\"My\n\t\t * work\" and \"Queue\" on one list). The route name alone lit both. An\n\t\t * entry with a `query` is active only when the address carries that\n\t\t * query; an entry without one steps back when a sibling's query\n\t\t * matches, so exactly one of them is marked.\n\t\t *\n\t\t * @param {object} item Menu entry.\n\t\t * @return {boolean} True when the entry is the current page.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query\n\t\t */\n\t\tisActive(item) {\n\t\t\tif (item.href || !item.route) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (item.route !== this.activeRouteName) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (item.query && typeof item.query === 'object') {\n\t\t\t\treturn this.queryMatches(item)\n\t\t\t}\n\t\t\tconst siblings = []\n\t\t\tfor (const entry of this.visibleItems) {\n\t\t\t\tsiblings.push(entry, ...this.visibleChildren(entry))\n\t\t\t}\n\t\t\treturn !siblings.some((entry) => entry !== item\n\t\t\t\t&& entry.route === item.route\n\t\t\t\t&& entry.query && typeof entry.query === 'object'\n\t\t\t\t&& this.queryMatches(entry))\n\t\t},\n\n\t\t/**\n\t\t * Whether the address carries every key of an entry's `query`.\n\t\t * Values are compared as strings, since an address has no numbers.\n\t\t *\n\t\t * @param {object} item Menu entry with a `query`.\n\t\t * @return {boolean} True when every declared key matches.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query\n\t\t */\n\t\tqueryMatches(item) {\n\t\t\tconst current = this.$route?.query ?? {}\n\t\t\tconst keys = Object.keys(item.query)\n\t\t\treturn keys.length > 0 && keys.every((key) => String(current[key] ?? '') === String(item.query[key]))\n\t\t},\n\n\t\t/**\n\t\t * Whether a menu entry renders as a `NcAppNavigationCaption`\n\t\t * (`type: \"caption\"`) rather than a clickable\n\t\t * `NcAppNavigationItem`. Caption entries ignore `route`, `href`,\n\t\t * `action`, `icon`, `count`, `children`, and `pinned`.\n\t\t *\n\t\t * @param {{ type?: string }} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\tisCaption(item) {\n\t\t\treturn item?.type === 'caption'\n\t\t},\n\n\t\t/**\n\t\t * Whether the host has registered a scoped slot named\n\t\t * `item-${id}-actions` for this menu item. Used to gate template\n\t\t * rendering of the per-item `#actions` slot pass-through so items\n\t\t * without a matching slot don't render an empty `<template #actions>`.\n\t\t *\n\t\t * @param {{ id: string }} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\thasItemActionsSlot(item) {\n\t\t\tif (!item?.id) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst name = `item-${item.id}-actions`\n\t\t\treturn Boolean((this.$slots && this.$slots[name])\n\t\t\t\t|| (this.$slots && this.$slots[name]))\n\t\t},\n\n\t\t/**\n\t\t * Resolve the count value to render in this entry's\n\t\t * `NcCounterBubble` (in the `#counter` slot of\n\t\t * `NcAppNavigationItem`).\n\t\t *\n\t\t * - Literal positive integer in `item.count` → return as-is.\n\t\t * - `item.count === \"auto\"` → look up the entry's resolved page;\n\t\t * when that page is `type: \"index\"` with a `register`/`schema`\n\t\t * in its `config`, return\n\t\t * `cnMenuCounts[register][schema] ?? null`.\n\t\t *\n\t\t * Returns `null` (no badge) when:\n\t\t * - `item.count` is unset or falsy\n\t\t * - The literal value is `0`\n\t\t * - `count === \"auto\"` but no matching index page resolves\n\t\t * (also emits a one-shot `console.warn`)\n\t\t * - The store has no entry for the resolved `(register, schema)`\n\t\t *\n\t\t * @param {object} item Menu entry descriptor.\n\t\t * @return {number|null} Count to render, or null for no badge.\n\t\t */\n\t\tresolveCount(item) {\n\t\t\tconst raw = item?.count\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof raw === 'number') {\n\t\t\t\treturn raw > 0 ? raw : null\n\t\t\t}\n\t\t\tif (raw !== 'auto') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst page = this.pageForItem(item)\n\t\t\tconst register = page?.config?.register\n\t\t\tconst schema = page?.config?.schema\n\t\t\tif (page?.type !== 'index' || !register || !schema) {\n\t\t\t\tthis.warnAutoCountMisconfigured(item)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst value = this.cnMenuCounts?.[register]?.[schema]\n\t\t\tif (typeof value !== 'number' || value <= 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * One-shot `console.warn` per menu-item id for misconfigured\n\t\t * `count: \"auto\"` entries (no resolvable index page). Keeps the\n\t\t * console quiet on re-renders.\n\t\t *\n\t\t * @param {{ id: string }} item Menu entry descriptor.\n\t\t * @return {void}\n\t\t * @private\n\t\t */\n\t\twarnAutoCountMisconfigured(item) {\n\t\t\tif (this._autoCountWarned.has(item.id)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis._autoCountWarned.add(item.id)\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn(`[CnAppNav] Menu entry \"${item.id}\" declares count: \"auto\" but has no resolvable index-type page with register + schema config — no badge will render.`)\n\t\t},\n\n\t\t/**\n\t\t * Look up an item's resolved page (`pages[]` entry whose `id`\n\t\t * matches the menu item's `route`) — used to decide whether the\n\t\t * NcAppNavigationItem should match its router-link `exact`.\n\t\t *\n\t\t * @param {object} item Menu item to resolve.\n\t\t * @return {object|null} Matching page entry, or null when the item\n\t\t * has no `route` or no page matches.\n\t\t */\n\t\tpageForItem(item) {\n\t\t\tif (!item.route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst pages = this.effectiveManifest?.pages ?? []\n\t\t\treturn pages.find((p) => p.id === item.route) ?? null\n\t\t},\n\n\t\t/**\n\t\t * Build the `:to` value for an `NcAppNavigationItem`. Action\n\t\t * items (`action: \"user-settings\"`) and `href` items return\n\t\t * `null` so the entry is NOT a vue-router link: action items\n\t\t * fall through to the click handler, `href` items render a real\n\t\t * anchor via `itemHref`. Route items return a named route.\n\t\t *\n\t\t * @param {object} item Menu item being rendered.\n\t\t * @return {object|null} A `{ name }` route object, or null for\n\t\t * action / href / route-less items.\n\t\t */\n\t\titemTo(item) {\n\t\t\tif (item.action) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (item.href) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (!item.route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\t// Carry optional query params so a nav entry can deep-link to a\n\t\t\t// pre-filtered index page (e.g. one entry per case type → Cases?caseType=…).\n\t\t\treturn item.query ? { name: item.route, query: item.query } : { name: item.route }\n\t\t},\n\n\t\t/**\n\t\t * Build the `:href` value for an `NcAppNavigationItem`. Returns\n\t\t * the item's `href` so the entry renders as a real anchor whose\n\t\t * destination is visible on hover and which gets the native link\n\t\t * cursor. `NcAppNavigationItem` adds `target=\"_blank\"` itself for\n\t\t * external (`scheme://`) URLs, so those open in a new tab while\n\t\t * internal app paths (e.g. `/index.php/apps/foo/`) navigate in the\n\t\t * same tab — no `window.open` interception. The `admin-settings`\n\t\t * action links to `adminSettingsHref` (absolute, so it opens in a\n\t\t * new tab); other action items carry no href. Returns `null` for\n\t\t * non-href items so the entry stays a router-link / button.\n\t\t *\n\t\t * @param {object} item Menu item being rendered.\n\t\t * @return {string|null} The destination URL, or null.\n\t\t */\n\t\titemHref(item) {\n\t\t\tif (item.action === 'admin-settings') {\n\t\t\t\treturn this.adminSettingsHref || null\n\t\t\t}\n\t\t\tif (item.action) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn item.href || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a menu group renders expanded. Local `openState` (set\n\t\t * by the chevron, a title click, or `pinGroupsEntered`) wins;\n\t\t * otherwise the group auto-expands when it contains the active\n\t\t * route — so deep-linking to a child page reveals which group it\n\t\t * lives in — and finally falls back to the manifest's `item.open`\n\t\t * for the initial render.\n\t\t *\n\t\t * @param {{ id: string, open?: boolean }} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\tisItemOpen(item) {\n\t\t\tconst local = this.openState[item.id]\n\t\t\tif (local !== undefined) {\n\t\t\t\treturn local\n\t\t\t}\n\t\t\tif (this.hasActiveChild(item)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Boolean(item.open)\n\t\t},\n\n\t\t/**\n\t\t * Whether any of a group's visible children is the active route.\n\t\t * Drives auto-expansion of the parent group on page load.\n\t\t *\n\t\t * @param {object} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\thasActiveChild(item) {\n\t\t\treturn this.visibleChildren(item).some((child) => this.isActive(child))\n\t\t},\n\n\t\t/**\n\t\t * Record a group's expand/collapse state. Bound to\n\t\t * NcAppNavigationItem's `@update:open` (chevron clicks) and\n\t\t * called from `onItemClick` for title clicks on route-less\n\t\t * group items.\n\t\t *\n\t\t * @param {{ id: string }} item Menu entry descriptor.\n\t\t * @param {boolean} value New open state.\n\t\t */\n\t\tsetItemOpen(item, value) {\n\t\t\tthis.openState[item.id] = value\n\t\t},\n\n\t\t/**\n\t\t * Pin open every group the active route just entered.\n\t\t *\n\t\t * Auto-expansion used to be read off the route alone, so a group\n\t\t * opened for a deep link collapsed the moment the route left it. A\n\t\t * group opened for you stays open until you close it. Only a group\n\t\t * the route ENTERS is pinned: one you closed while on one of its\n\t\t * children stays closed while you move between them.\n\t\t *\n\t\t * @param {string|null} previousRoute The active route before this change, or undefined at first render.\n\t\t */\n\t\tpinGroupsEntered(previousRoute) {\n\t\t\tfor (const item of this.visibleItems) {\n\t\t\t\tconst children = this.visibleChildren(item)\n\t\t\t\tif (children.length === 0 || !this.hasActiveChild(item)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst wasInside = children.some((child) => child.route === previousRoute)\n\t\t\t\tif (!wasInside || this.openState[item.id] === undefined) {\n\t\t\t\t\tthis.openState[item.id] = true\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Click handler. Dispatch order: action keyword → group\n\t\t * open/toggle. For `action: \"user-settings\"` invokes the injected\n\t\t * `cnOpenUserSettings` (provided by CnAppRoot) and prevents\n\t\t * default; `action: \"admin-settings\"` is a real link to\n\t\t * `/settings/admin/<appId>` (see `itemHref`) and is left to the\n\t\t * browser; for `action:\n\t\t * \"replay-walkthrough\"` invokes the injected\n\t\t * `cnReplayWalkthrough(item.tourId)` and prevents default. `href`\n\t\t * items with no children are NOT handled here — they render a real\n\t\t * anchor via `itemHref`, so the browser navigates natively\n\t\t * (external URLs open in a new tab, internal app paths in the same\n\t\t * tab).\n\t\t *\n\t\t * A group's children visibility on click depends on whether the\n\t\t * group ITSELF has a destination:\n\t\t * - Route-less, href-less: a pure group header, so its anchor is\n\t\t * a dead `#` link and the click's only job is to TOGGLE the\n\t\t * children open/closed (same effect as the collapse chevron).\n\t\t * - Carries a `route` or `href`: the click is a real navigation\n\t\t * (handled natively by `:to` / the anchor, not prevented here),\n\t\t * and this additionally forces the group OPEN — never closed —\n\t\t * so the reader lands on the group's own page without a second\n\t\t * click to see what else it holds. The collapse chevron is the\n\t\t * only control that can close such a group again.\n\t\t *\n\t\t * @param {object} item Menu item being clicked.\n\t\t * @param {Event} [event] Native click event (used to call\n\t\t * preventDefault for action / group links).\n\t\t */\n\t\tonItemClick(item, event) {\n\t\t\tif (item.action === 'user-settings') {\n\t\t\t\tif (event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\tthis.cnOpenUserSettings()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (item.action === 'admin-settings') {\n\t\t\t\t// ADR-079: a real link to Nextcloud's own settings (see\n\t\t\t\t// itemHref), followed natively in a new tab. Without an app\n\t\t\t\t// id there is no href, and the click does nothing.\n\t\t\t\tif (!this.adminSettingsHref && event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (item.action === 'replay-walkthrough') {\n\t\t\t\tif (event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\tthis.cnReplayWalkthrough(item.tourId)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.visibleChildren(item).length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!item.route && !item.href) {\n\t\t\t\tif (event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\tthis.setItemOpen(item, !this.isItemOpen(item))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The item is ALSO a real destination: let the click navigate\n\t\t\t// natively and reveal the children alongside it.\n\t\t\tthis.setItemOpen(item, true)\n\t\t},\n\n\t\t/**\n\t\t * Click handler for the auto-prepended Personal-settings entry in\n\t\t * the settings foldout. Invokes the injected `cnOpenUserSettings`\n\t\t * (provided by CnAppRoot → opens the host's NcAppSettingsDialog).\n\t\t * No-op inject when mounted standalone.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonPersonalSettingsClick() {\n\t\t\tthis.cnOpenUserSettings()\n\t\t},\n\n\t\t/**\n\t\t * Click handler for the manifest-declared primary action. Emits\n\t\t * `@primary-action` AND the back-compat `@primary-action-click`\n\t\t * event for host-side handling. Navigation is the link's own: an\n\t\t * `href` or `route` action renders as a real link (see\n\t\t * `primaryActionLink`), so the browser or router follows it after\n\t\t * these events. Not invoked when the host overrides the\n\t\t * `#primary-action` slot (it provides its own handler).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonPrimaryActionClick() {\n\t\t\tconst action = this.activePrimaryAction\n\t\t\tif (!action) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst payload = {\n\t\t\t\tid: action.id,\n\t\t\t\tlabel: action.label,\n\t\t\t\ticon: action.icon,\n\t\t\t\troute: action.route,\n\t\t\t\thref: action.href,\n\t\t\t\tpayload: action.payload,\n\t\t\t\tpage: this.$route?.name,\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event primary-action Emitted when the resolved primary-action button is clicked. Payload includes the action descriptor (`{ id, label, icon, route, href, payload }`) plus the current `page` (route name) for host dispatchers.\n\t\t\t * @type {{ id?: string, label: string, icon?: string, route?: string, href?: string, payload?: unknown, page?: string }}\n\t\t\t */\n\t\t\tthis.$emit('primary-action', payload)\n\t\t\t/**\n\t\t\t * @event primary-action-click Back-compat alias for `@primary-action`. Payload is the resolved primary action object as declared in the manifest.\n\t\t\t */\n\t\t\tthis.$emit('primary-action-click', action)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* The brand block at the top of the navigation. */\n.cn-app-nav__brand {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: calc(2.5 * var(--default-grid-baseline));\n\tmin-width: 0;\n\tpadding: calc(2 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline)) calc(3 * var(--default-grid-baseline));\n}\n\n.cn-app-nav__brand-logo {\n\tflex: none;\n\theight: 34px;\n\tmax-width: 50%;\n\tobject-fit: contain;\n\twidth: auto;\n}\n\n.cn-app-nav__brand-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tline-height: 1.15;\n\tmin-width: 0;\n}\n\n.cn-app-nav__brand-name {\n\tcolor: var(--color-main-text);\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n\toverflow-wrap: anywhere;\n}\n\n.cn-app-nav__brand-caption {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\toverflow-wrap: anywhere;\n}\n\n/*\n * The legacy `icon-*` classes in Nextcloud render a background-image\n * with a hardcoded dark fill, so they stay grey when an entry becomes\n * active (text turns white against the primary-element background).\n * Force the icon to white in the active state to match the label.\n * Only applies when the icon is provided via the `icon` prop (CSS\n * class) — items using a `<template #icon>` MDI component already\n * inherit `currentColor`.\n */\n.app-navigation-entry.active .app-navigation-entry-icon[class*=\"icon-\"] {\n\tfilter: brightness(0) invert(1);\n}\n\n/* The link form of the primary action, matching NcAppNavigationNew's scoped layout. */\n.cn-app-nav__primary-action {\n\tdisplay: block;\n\tpadding: calc(var(--default-grid-baseline, 4px) * 2);\n}\n\n/*\n * Footer-section items (section: \"footer\") render in NcAppNavigation's\n * #footer slot, outside the scroll container, so they stay visible above\n * the settings foldout regardless of menu length. (The interim\n * `pinned`-prop approach kept them inside the scrollable list, where\n * `margin-top: auto` only bottom-pins while the list does not overflow —\n * long menus showed Documentation / Features & roadmap mid-scroll.)\n *\n * No `padding` here on purpose: it comes from NcAppNavigationList's own\n * `var(--app-navigation-padding)`, so it stays in step with the main list.\n *\n * As a direct `> ul` child of `.app-navigation__content`, NC makes it a\n * shrinkable, scrollable flex item, and NcAppNavigationList adds\n * `overflow-y: auto` besides. That let a two-entry list be squeezed below its\n * content and grow a scrollbar on a short menu, so opt out of both.\n */\n.cn-app-nav__footer-list {\n\tlist-style: none;\n\tmargin: 0;\n\tflex-shrink: 0 !important;\n\toverflow: visible !important;\n}\n\n/*\n * Align the settings foldout's items with the main/footer list items,\n * whose icons sit at a 16px inset. The foldout's items live in a bare\n * <ul> inside NcAppNavigationSettings' panel (`#app-settings`, padding\n * 3px), so without this they start ~5px further left and the icon\n * column looks ragged. (The native settings-toggle button sits ~1px\n * left of the 16px baseline, but NC owns that with an !important\n * shorthand — a sub-pixel difference we leave to the native component.)\n */\n.cn-app-nav__settings-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding-inline-start: 5px;\n}\n</style>\n"],"names":["_createBlock","_Fragment","_renderList","_openBlock","_createElementBlock","_resolveDynamicComponent","_createVNode","_renderSlot","_mergeProps","_toDisplayString"],"mappings":"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"}
1
+ {"version":3,"file":"CnAppNav.vue3.js","sources":["../../../../src/components/CnAppNav/CnAppNav.vue?vue&type=template&id=2b18f46f&lang.js"],"sourcesContent":["<!--\n CnAppNav — manifest-driven app navigation.\n\n Renders the manifest's `menu[]` array as a Nextcloud app navigation\n (NcAppNavigation + NcAppNavigationItem). One level of nested\n `children[]` is supported. Items are sorted by `order` at BOTH levels —\n top-level entries and a group's children — and items without an order\n render last. Items with a `permission` are filtered against\n the `permissions` prop — when the prop is omitted, all items render.\n\n An optional primary action renders above the main list as an\n NcAppNavigationNew button (e.g. a \"new\" button or an active-context\n switcher). Hosts supply it via the `#primary-action` slot (full\n control over dynamic content + click handling); a static\n `nav.primaryAction` ({ label, icon?, route?, href? }) manifest field\n is the declarative fallback. The slot wins when both are present;\n nothing renders when neither is.\n\n Items split into three RENDERED groups by `section`, plus one section\n this component deliberately renders nowhere:\n - `section: \"main\"` (default) — top of the navigation, scrollable.\n - `section: \"footer\"` — rendered in NcAppNavigation's `#footer` slot,\n OUTSIDE the scrollable list and directly above the settings foldout,\n so they stay visible regardless of how long the main menu is. (The\n earlier `pinned`-prop approach kept them inside the scroll container:\n `margin-top: auto` only bottom-pins while the list does not overflow,\n so apps with long menus showed them mid-scroll.) For always-visible,\n non-settings links: Documentation, Features & Roadmap, About.\n - `section: \"settings\"` — rendered INSIDE an NcAppNavigationSettings\n foldout (the NC-native gear-icon button that slides a panel open).\n A \"Personal settings\" entry is auto-prepended at the top of the\n foldout (opens the host app's NcAppSettingsDialog via\n cnOpenUserSettings); opt out with `nav.includePersonalSettings:\n false`. Right below it, an \"Admin settings\" entry is auto-prepended\n for INSTANCE ADMINS only — gated on the `isAdmin` prop (computed by\n CnAppRoot from `getCurrentUser()?.isAdmin`). It is a LINK to\n `/settings/admin/<appId>` that opens in a NEW TAB and carries an\n open-in-new marker (the destination is outside the app — the marker\n says so up front, the new tab keeps the app open), not a modal: per\n ADR-079 app-level\n configuration lives in Nextcloud's own settings framework, which\n authorizes it server-side. `isAdmin` gates VISIBILITY only and is\n never an authorization decision; it is also NOT interchangeable with\n `isOwner`, which means \"owns this app\" rather than \"administers this\n instance\" and keeps its own separate uses. The\n foldout mounts whenever there are settings items OR personal\n settings is enabled — so every app shows a Settings gear with at\n least Personal settings. It is only fully suppressed when there are\n no settings items AND `nav.includePersonalSettings: false`.\n - `section: \"integrations\"` — rendered by this component NOWHERE. Per\n ADR-110 these are the links that LEAVE this app for another one, and\n they belong in the Integrations section at the bottom of the per-user\n settings modal, which CnAppRoot owns. A link to another app cannot be\n the active route and carries no counter, so putting it in the nav\n makes another app's capability read as this app's feature. The one\n deep link that stays in the navigation is the auto-prepended Admin\n settings above — apps never declare that one themselves.\n\n Manifest and translate are injected from CnAppRoot by default but can\n also be passed as props for standalone use without CnAppRoot. Props\n win over inject when both are present.\n\n Items can opt out of routing in favour of a built-in action by\n setting `action` on the manifest entry. Supported keywords:\n `\"user-settings\"` invokes the `cnOpenUserSettings` provide-injected\n by CnAppRoot, which opens the host app's NcAppSettingsDialog modal;\n `\"admin-settings\"` opens `/settings/admin/<appId>` in a new tab (ADR-079);\n `\"replay-walkthrough\"` invokes `cnReplayWalkthrough` (optionally\n with the item's `tourId`) to re-run the product walkthrough from\n the first step (ADR-043). Both `route` and `href` are ignored when\n `action` is set. The injects default to a no-op so CnAppNav stays\n usable standalone (without a CnAppRoot ancestor).\n\n See REQ-JMR-004 of the json-manifest-renderer specification.\n-->\n<template>\n\t<NcAppNavigation :aria-label=\"ariaLabel\" data-testid=\"cn-nav\">\n\t\t<template v-if=\"$slots.search || $slots.brand || resolvedBrand\" #search>\n\t\t\t<!--\n\t\t\t\t@slot brand\n\t\t\t\t@description Replace the brand block at the very top of the\n\t\t\t\tnavigation. Default content: the logo, name and caption from\n\t\t\t\tthe `brand` prop or the manifest's `nav.brand`. Renders\n\t\t\t\tnothing when neither is set.\n\t\t\t\t@binding {object|null} brand The resolved brand (`{ logo, name, caption, alt }`), or null.\n\t\t\t-->\n\t\t\t<slot name=\"brand\" :brand=\"resolvedBrand\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"resolvedBrand\"\n\t\t\t\t\tclass=\"cn-app-nav__brand\"\n\t\t\t\t\tdata-testid=\"cn-nav-brand\">\n\t\t\t\t\t<!-- Decorative beside a name: the name already says whose\n\t\t\t\t\t app this is, and an alt text would say it twice. A logo\n\t\t\t\t\t on its own carries the name as its alt text. -->\n\t\t\t\t\t<img\n\t\t\t\t\t\tv-if=\"resolvedBrand.logo\"\n\t\t\t\t\t\tclass=\"cn-app-nav__brand-logo\"\n\t\t\t\t\t\t:src=\"resolvedBrand.logo\"\n\t\t\t\t\t\t:alt=\"resolvedBrand.name ? '' : resolvedBrand.alt\">\n\t\t\t\t\t<span v-if=\"resolvedBrand.name || resolvedBrand.caption\" class=\"cn-app-nav__brand-text\">\n\t\t\t\t\t\t<strong v-if=\"resolvedBrand.name\" class=\"cn-app-nav__brand-name\">{{ resolvedBrand.name }}</strong>\n\t\t\t\t\t\t<span v-if=\"resolvedBrand.caption\" class=\"cn-app-nav__brand-caption\">{{ resolvedBrand.caption }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</slot>\n\t\t\t<!--\n\t\t\t\t@slot search\n\t\t\t\t@description Forwarded into NcAppNavigation's #search slot.\n\t\t\t\tHosts mount NcAppNavigationSearch here. When unset, no search\n\t\t\t\tinput renders inside the navigation.\n\t\t\t-->\n\t\t\t<slot name=\"search\" />\n\t\t</template>\n\t\t<!-- @slot primary-action Optional primary action rendered above the\n\t\t main list as NcAppNavigation's top region (e.g. an app's \"new\"\n\t\t button or an active-context switcher). When provided, it takes\n\t\t precedence over the manifest-declared primaryAction (page-scoped\n\t\t OR nav root). Omitted entirely when neither the slot nor any\n\t\t resolvable primaryAction is present. -->\n\t\t<slot name=\"primary-action\">\n\t\t\t<!-- A primary action with an href or route is a real link.\n\t\t\t NcAppNavigationNew cannot render one, so this mirrors its\n\t\t\t wrapper and button. -->\n\t\t\t<div\n\t\t\t\tv-if=\"activePrimaryAction && primaryActionLink\"\n\t\t\t\tclass=\"app-navigation-new cn-app-nav__primary-action\"\n\t\t\t\tdata-testid=\"cn-nav-primary-action\">\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\twide\n\t\t\t\t\tv-bind=\"primaryActionLink\"\n\t\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<component :is=\"primaryActionIconComponent\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ resolveLabel(activePrimaryAction) }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<NcAppNavigationNew\n\t\t\t\tv-else-if=\"activePrimaryAction\"\n\t\t\t\t:text=\"resolveLabel(activePrimaryAction)\"\n\t\t\t\tdata-testid=\"cn-nav-primary-action\"\n\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<component :is=\"primaryActionIconComponent\" :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t</NcAppNavigationNew>\n\t\t</slot>\n\t\t<template #list>\n\t\t\t<template v-for=\"item in mainItems\">\n\t\t\t\t<NcAppNavigationCaption\n\t\t\t\t\tv-if=\"isCaption(item)\"\n\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t:data-testid=\"`cn-nav-caption-${item.id}`\" />\n\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\tv-else\n\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t:to=\"itemTo(item)\"\n\t\t\t\t\t:href=\"itemHref(item)\"\n\t\t\t\t\t:icon=\"cssIconClass(item)\"\n\t\t\t\t\t:active=\"isActive(item)\"\n\t\t\t\t\t:pinned=\"Boolean(item.pinned)\"\n\t\t\t\t\t:allowCollapse=\"visibleChildren(item).length > 0\"\n\t\t\t\t\t:open=\"isItemOpen(item)\"\n\t\t\t\t\t:data-testid=\"`cn-nav-entry-${item.id}`\"\n\t\t\t\t\t:data-cn-route=\"item.route\"\n\t\t\t\t\t@update:open=\"setItemOpen(item, $event)\"\n\t\t\t\t\t@click=\"onItemClick(item, $event)\">\n\t\t\t\t\t<template v-if=\"mdiIconComponent(item) || isRichIcon(item) || isRegistryIcon(item) || isUnresolvedIcon(item)\" #icon>\n\t\t\t\t\t\t<component :is=\"mdiIconComponent(item)\" v-if=\"mdiIconComponent(item)\" :size=\"20\" />\n\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(item)\" :size=\"20\" />\n\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"item.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"resolveCount(item)\" #counter>\n\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t:count=\"resolveCount(item)\"\n\t\t\t\t\t\t\t:active=\"isActive(item)\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"hasItemActionsSlot(item)\" #actions>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot `item-${item.id}-actions`\n\t\t\t\t\t\t\t@description Per-item scoped slot whose content lands inside\n\t\t\t\t\t\t\tthe NcAppNavigationItem's #actions slot for the menu entry\n\t\t\t\t\t\t\twith that id. Use it for inline NcActions menus (e.g. an\n\t\t\t\t\t\t\titem-level \"Pin\" button). Scope receives the menu item.\n\t\t\t\t\t\t\t@binding {object} item The menu item descriptor.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot :name=\"`item-${item.id}-actions`\" :item=\"item\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-for=\"child in visibleChildren(item)\"\n\t\t\t\t\t\t:key=\"child.id\"\n\t\t\t\t\t\t:name=\"resolveLabel(child)\"\n\t\t\t\t\t\t:to=\"itemTo(child)\"\n\t\t\t\t\t\t:href=\"itemHref(child)\"\n\t\t\t\t\t\t:icon=\"cssIconClass(child)\"\n\t\t\t\t\t\t:active=\"isActive(child)\"\n\t\t\t\t\t\t:pinned=\"Boolean(child.pinned)\"\n\t\t\t\t\t\t:data-testid=\"`cn-nav-entry-${child.id}`\"\n\t\t\t\t\t\t:data-cn-route=\"child.route\"\n\t\t\t\t\t\t@click=\"onItemClick(child, $event)\">\n\t\t\t\t\t\t<template v-if=\"mdiIconComponent(child) || isRichIcon(child) || isRegistryIcon(child) || isUnresolvedIcon(child)\" #icon>\n\t\t\t\t\t\t\t<component :is=\"mdiIconComponent(child)\" v-if=\"mdiIconComponent(child)\" :size=\"20\" />\n\t\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(child)\" :size=\"20\" />\n\t\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"child.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-if=\"resolveCount(child)\" #counter>\n\t\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t\t:count=\"resolveCount(child)\"\n\t\t\t\t\t\t\t\t:active=\"isActive(child)\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t</NcAppNavigationItem>\n\t\t\t</template>\n\t\t</template>\n\t\t<template v-if=\"footerItems.length > 0 || showSettingsFoldout\" #footer>\n\t\t\t<!-- Footer-section entries (Documentation, Features & Roadmap,\n\t\t\t About) live in NcAppNavigation's #footer slot — OUTSIDE the\n\t\t\t scrollable list — so they stay visible above the settings\n\t\t\t foldout no matter how long the main menu is. The pinned-prop\n\t\t\t approach only bottom-pinned while the list did not overflow.\n\n\t\t\t NcAppNavigationList, not a bare <ul>: its padding, gap and\n\t\t\t hover highlight are scoped to itself, so they survive being\n\t\t\t slotted. The main list's inset comes from NcAppNavigation's\n\t\t\t scope, which slot content does not carry. -->\n\t\t\t<NcAppNavigationList v-if=\"footerItems.length > 0\" class=\"cn-app-nav__footer-list\">\n\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\tv-for=\"item in footerItems\"\n\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t:to=\"itemTo(item)\"\n\t\t\t\t\t:href=\"itemHref(item)\"\n\t\t\t\t\t:icon=\"cssIconClass(item)\"\n\t\t\t\t\t:active=\"isActive(item)\"\n\t\t\t\t\t:data-testid=\"`cn-nav-entry-${item.id}`\"\n\t\t\t\t\t:data-cn-route=\"item.route\"\n\t\t\t\t\t@click=\"onItemClick(item, $event)\">\n\t\t\t\t\t<template v-if=\"mdiIconComponent(item) || isRichIcon(item) || isRegistryIcon(item) || isUnresolvedIcon(item)\" #icon>\n\t\t\t\t\t\t<component :is=\"mdiIconComponent(item)\" v-if=\"mdiIconComponent(item)\" :size=\"20\" />\n\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(item)\" :size=\"20\" />\n\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"item.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"resolveCount(item)\" #counter>\n\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t:count=\"resolveCount(item)\"\n\t\t\t\t\t\t\t:active=\"isActive(item)\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcAppNavigationItem>\n\t\t\t</NcAppNavigationList>\n\t\t\t<!-- Settings foldout (section: \"settings\" items). NC-native\n\t\t\t gear-icon button that slides open a panel; the first entry\n\t\t\t is an auto-prepended \"Personal settings\" that opens the\n\t\t\t host app's NcAppSettingsDialog via cnOpenUserSettings. -->\n\t\t\t<NcAppNavigationSettings\n\t\t\t\tv-if=\"showSettingsFoldout\"\n\t\t\t\t:name=\"settingsFoldoutLabel\"\n\t\t\t\tdata-testid=\"cn-nav-settings\">\n\t\t\t\t<ul class=\"cn-app-nav__settings-list\">\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"includePersonalSettings\"\n\t\t\t\t\t\t:name=\"personalSettingsLabel\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-personal-settings\"\n\t\t\t\t\t\t@click=\"onPersonalSettingsClick\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Cog :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"roadmapEntry\"\n\t\t\t\t\t\t:name=\"roadmapEntry.label\"\n\t\t\t\t\t\t:to=\"roadmapEntry.to\"\n\t\t\t\t\t\t:href=\"roadmapEntry.href\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-roadmap\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<MapMarkerPath :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"documentationEntry\"\n\t\t\t\t\t\t:name=\"documentationEntry.label\"\n\t\t\t\t\t\t:href=\"documentationEntry.href\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-documentation\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<BookOpenVariant :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<!-- The target is Nextcloud's OWN settings area, not an\n\t\t\t\t\t in-app route: a new tab keeps the app open, and the\n\t\t\t\t\t trailing open-in-new marker tells the user they are\n\t\t\t\t\t leaving the app before they click. The new tab comes\n\t\t\t\t\t from the href being ABSOLUTE (see adminSettingsHref) —\n\t\t\t\t\t NcAppNavigationItem ignores a `target` attr and only\n\t\t\t\t\t sets target=\"_blank\" for scheme-prefixed hrefs. -->\n\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\tv-if=\"showAdminSettingsLink\"\n\t\t\t\t\t\t:name=\"adminSettingsLabel\"\n\t\t\t\t\t\t:href=\"adminSettingsHref\"\n\t\t\t\t\t\tdata-testid=\"cn-nav-admin-settings\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<ShieldAccountOutline :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template #counter>\n\t\t\t\t\t\t\t<OpenInNew :size=\"16\" :title=\"opensInNewTabHint\" data-testid=\"cn-nav-admin-settings-external\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t<template v-for=\"item in settingsItems\">\n\t\t\t\t\t\t<NcAppNavigationCaption\n\t\t\t\t\t\t\tv-if=\"isCaption(item)\"\n\t\t\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-nav-caption-${item.id}`\" />\n\t\t\t\t\t\t<NcAppNavigationItem\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t\t\t:name=\"resolveLabel(item)\"\n\t\t\t\t\t\t\t:to=\"itemTo(item)\"\n\t\t\t\t\t\t\t:href=\"itemHref(item)\"\n\t\t\t\t\t\t\t:icon=\"cssIconClass(item)\"\n\t\t\t\t\t\t\t:active=\"isActive(item)\"\n\t\t\t\t\t\t\t:data-cn-route=\"item.route\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-nav-entry-${item.id}`\"\n\t\t\t\t\t\t\t@click=\"onItemClick(item, $event)\">\n\t\t\t\t\t\t\t<template v-if=\"mdiIconComponent(item) || isRichIcon(item) || isRegistryIcon(item) || isUnresolvedIcon(item)\" #icon>\n\t\t\t\t\t\t\t\t<component :is=\"mdiIconComponent(item)\" v-if=\"mdiIconComponent(item)\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<HelpCircleOutline v-else-if=\"isUnresolvedIcon(item)\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<CnMenuItemIcon v-else :icon=\"item.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t<template v-if=\"resolveCount(item)\" #counter>\n\t\t\t\t\t\t\t\t<NcCounterBubble\n\t\t\t\t\t\t\t\t\t:count=\"resolveCount(item)\"\n\t\t\t\t\t\t\t\t\t:active=\"isActive(item)\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcAppNavigationItem>\n\t\t\t\t\t</template>\n\t\t\t\t</ul>\n\t\t\t</NcAppNavigationSettings>\n\t\t</template>\n\t</NcAppNavigation>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcAppNavigation, NcAppNavigationCaption, NcAppNavigationItem, NcAppNavigationList, NcAppNavigationNew, NcAppNavigationSettings, NcButton, NcCounterBubble } from '@nextcloud/vue'\nimport { toRaw } from 'vue'\nimport BookOpenVariant from 'vue-material-design-icons/BookOpenVariant.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\n// ADR-077 rule 4: visible fallback for an unresolvable icon name.\nimport HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'\nimport MapMarkerPath from 'vue-material-design-icons/MapMarkerPath.vue'\nimport OpenInNew from 'vue-material-design-icons/OpenInNew.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport ShieldAccountOutline from 'vue-material-design-icons/ShieldAccountOutline.vue'\nimport { ICON_MAP } from '../CnIcon/CnIcon.vue'\nimport CnMenuItemIcon from '../CnMenuWidget/CnMenuItemIcon.vue'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { isSvgPath } from '../../utils/iconUtils.js'\nimport { passesContextPredicates } from '../../utils/visibleIfContext.js'\n// The legacy `icon-*` → MDI bridge lives beside CnIcon now, so the menu EDITOR\n// resolves a seeded `icon-comment` the same way this nav does. It used to be a\n// map local to this file, which is why the nav drew a proper glyph and the\n// editor drew a help-circle \"?\" for the very same manifest value.\nimport { bridgedMdiForCssIcon } from '../CnIcon/cssIconBridge.js'\nimport { getSemanticIconComponent } from '../CnIcon/semanticIcons.js'\nimport { hasRegistryIcon, isCustomIconUrl } from '../CnWidgetGrid/widgetIcons.js'\n\n/**\n * Order two menu entries by their manifest `order`.\n *\n * Entries carrying an `order` come first, ascending; entries without one render\n * last and keep their relative order. Used at BOTH nav levels so a group's\n * children obey the same rule as the top-level list.\n *\n * @param {object} a First entry.\n * @param {object} b Second entry.\n * @return {number} Comparator result.\n */\nfunction byManifestOrder(a, b) {\n\tconst aHas = typeof a.order === 'number'\n\tconst bHas = typeof b.order === 'number'\n\tif (aHas && !bHas) {\n\t\treturn -1\n\t}\n\tif (!aHas && bHas) {\n\t\treturn 1\n\t}\n\tif (!aHas && !bHas) {\n\t\treturn 0\n\t}\n\treturn a.order - b.order\n}\n\nexport default {\n\tname: 'CnAppNav',\n\n\tcomponents: {\n\t\tNcAppNavigation,\n\t\tNcAppNavigationCaption,\n\t\tNcAppNavigationItem,\n\t\tNcAppNavigationList,\n\t\tNcAppNavigationNew,\n\t\tNcAppNavigationSettings,\n\t\tNcButton,\n\t\tNcCounterBubble,\n\t\tCnMenuItemIcon,\n\t\tCog,\n\t\tHelpCircleOutline,\n\t\tShieldAccountOutline,\n\t\tMapMarkerPath,\n\t\tBookOpenVariant,\n\t\tOpenInNew,\n\t},\n\n\tinject: {\n\t\tcnManifest: { default: null },\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * Provided by CnAppRoot — opens the host app's\n\t\t * NcAppSettingsDialog. Defaults to a no-op so CnAppNav is\n\t\t * still usable when mounted outside a CnAppRoot ancestor;\n\t\t * the click silently does nothing in that case rather than\n\t\t * throwing.\n\t\t */\n\t\tcnOpenUserSettings: { default: () => () => {} },\n\t\t/**\n\t\t * Provided by CnAppRoot — the consuming app's slug, used to build\n\t\t * the \"Admin settings\" link target `/settings/admin/<appId>`.\n\t\t * Overridden by the explicit `appId` prop when both are present.\n\t\t * Null outside a CnAppRoot ancestor, in which case the link is\n\t\t * suppressed (ADR-079 §2).\n\t\t */\n\t\tcnAppId: { default: null },\n\t\t/**\n\t\t * Provided by CnAppRoot — restarts the product walkthrough (ADR-043)\n\t\t * from the first step. Bound to menu entries declaring\n\t\t * `action: \"replay-walkthrough\"` (optionally with a `tourId`).\n\t\t * Defaults to a no-op so CnAppNav stays usable standalone.\n\t\t */\n\t\tcnReplayWalkthrough: { default: () => () => {} },\n\t\t/**\n\t\t * Provided by CnAppRoot — reactive `{ [register]: { [schema]: number } }`\n\t\t * map populated from `useObjectStore().totals` for every\n\t\t * `count: \"auto\"` menu entry whose resolved page is `type: \"index\"`\n\t\t * with `register + schema` in its config. Defaults to an empty\n\t\t * object so `resolveCount` returns `null` (no badge) when CnAppNav\n\t\t * is mounted outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {{ [register: string]: { [schema: string]: number } }}\n\t\t */\n\t\tcnMenuCounts: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Manifest object. Falls back to injected `cnManifest`. Provide\n\t\t * explicitly when mounting CnAppNav outside of CnAppRoot.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tmanifest: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`,\n\t\t * which itself defaults to an identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * List of permission strings the current user holds. Items\n\t\t * declaring a `permission` only render when their permission\n\t\t * appears in this list. When the prop is omitted (or empty),\n\t\t * all items are visible regardless of their permission field.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tpermissions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user is an OWNER of this app\n\t\t * (admin-settings-owner-gating capability). Computed by CnAppRoot\n\t\t * from `currentUserGroups` ∩ `permissions.owners` and/or a manifest\n\t\t * `runtime.user` owner signal — deliberately NOT `OC.isUserAdmin()`.\n\t\t * Read by nothing in this component: the auto-included \"Admin\n\t\t * settings\" entry is gated on `isAdmin` alone (see the file header\n\t\t * and `showAdminSettingsLink`). The prop stays because CnAppRoot\n\t\t * binds it as published API and owner-gating is its own question,\n\t\t * not the instance-admin one.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tisOwner: { // eslint-disable-line vue/no-unused-properties -- published prop bound by CnAppRoot; owner-gating is decided upstream, not here\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user administers this Nextcloud instance.\n\t\t * Computed by CnAppRoot from `getCurrentUser()?.isAdmin`\n\t\t * (`@nextcloud/auth`) — NOT the legacy `OC.isUserAdmin()` global,\n\t\t * and NOT interchangeable with `isOwner`, which is about owning a\n\t\t * given app rather than administering the instance (ADR-079 §3).\n\t\t *\n\t\t * Gates VISIBILITY of the \"Admin settings\" link only. It is not an\n\t\t * authorization decision and MUST NOT be used as one: the boundary\n\t\t * is Nextcloud's settings framework, which refuses\n\t\t * `/settings/admin/<app>` server-side for non-admins. Defaults to\n\t\t * `false` so CnAppNav mounted standalone never shows the link.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tisAdmin: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * App id used to build the \"Admin settings\" link target\n\t\t * (`/settings/admin/<appId>`). Falls back to the injected\n\t\t * `cnAppId` provided by CnAppRoot. With neither available the\n\t\t * link is suppressed rather than pointing at a broken URL.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tappId: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the navigation landmark, forwarded to\n\t\t * `NcAppNavigation`'s `aria-label`.\n\t\t *\n\t\t * `@nextcloud/vue` 9 requires `ariaLabel` or `ariaLabelledby` and warns\n\t\t * when neither is set: a `<nav>` with no accessible name is announced as\n\t\t * an unlabelled landmark, so a screen-reader user tabbing the landmark\n\t\t * list cannot tell it from any other nav on the page (WCAG 2.4.6 /\n\t\t * 1.3.1). The default covers every consumer without a code change; pass\n\t\t * your own when an app shows more than one navigation, since the point\n\t\t * of the name is to tell them apart.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Main navigation'),\n\t\t},\n\n\t\t/**\n\t\t * The brand block at the top of the navigation:\n\t\t * `{ logo?, name?, caption?, alt? }`. `logo` is an image URL, `name`\n\t\t * the app or organisation name, `caption` a line under it. Falls back\n\t\t * to the manifest's `nav.brand`. `null` (the default) with no manifest\n\t\t * brand renders nothing.\n\t\t *\n\t\t * @type {{logo?: string, name?: string, caption?: string, alt?: string}|null}\n\t\t */\n\t\tbrand: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['primary-action', 'primary-action-click'],\n\n\t/**\n\t * The navigation's own state: which groups are open, and which entry\n\t * of a route the reader last used.\n\t *\n\t * @return {object} The state.\n\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t */\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * Per-item expand/collapse state for menu groups, keyed by\n\t\t\t * item id. Seeded lazily from the manifest's `item.open` by\n\t\t\t * `isItemOpen`; written by the collapse chevron (via\n\t\t\t * `@update:open`), by title clicks on route-less group items\n\t\t\t * (which TOGGLE open/closed), and by title clicks on a group\n\t\t\t * that ALSO carries a route or href (which force it OPEN,\n\t\t\t * alongside the navigation — see `onItemClick`).\n\t\t\t */\n\t\t\topenState: {},\n\n\t\t\t/**\n\t\t\t * Per route name, the key of the entry that was last active on\n\t\t\t * that route in this session. Read by `subRouteParent` to keep\n\t\t\t * \"the list I came from\" marked on a page below it. In memory\n\t\t\t * only: a reload starts empty and falls back to menu order.\n\t\t\t *\n\t\t\t * @type {Record<string, string>}\n\t\t\t */\n\t\t\tlastActiveEntryByRoute: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\teffectiveManifest() {\n\t\t\treturn this.manifest ?? this.cnManifest\n\t\t},\n\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The brand to draw: the `brand` prop, else the manifest's\n\t\t * `nav.brand`. Name and caption go through the translate function,\n\t\t * like every other manifest label. Null when there is nothing to\n\t\t * show, which is what keeps the block out of a navigation that\n\t\t * declares no brand.\n\t\t *\n\t\t * @return {{logo: string, name: string, caption: string, alt: string}|null} The brand, or null.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-navigation-brand\n\t\t */\n\t\tresolvedBrand() {\n\t\t\tconst declared = this.brand ?? this.effectiveManifest?.nav?.brand\n\t\t\tif (!declared || typeof declared !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst text = (value) => (typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : '')\n\t\t\tconst brand = {\n\t\t\t\tlogo: typeof declared.logo === 'string' ? declared.logo : '',\n\t\t\t\tname: text(declared.name),\n\t\t\t\tcaption: text(declared.caption),\n\t\t\t\talt: text(declared.alt),\n\t\t\t}\n\t\t\treturn (brand.logo || brand.name || brand.caption) ? brand : null\n\t\t},\n\n\t\t/**\n\t\t * Manifest-declared root-level primary action (`nav.primaryAction`)\n\t\t * — retained for backwards compatibility with code that reads this\n\t\t * computed. New code SHOULD read `activePrimaryAction` instead,\n\t\t * which also handles the page-scoped override.\n\t\t *\n\t\t * @return {object|null} `{ label, icon?, route?, href?, id?, payload? }` or null.\n\t\t */\n\t\tprimaryAction() {\n\t\t\treturn this.effectiveManifest?.nav?.primaryAction ?? null\n\t\t},\n\n\t\t/**\n\t\t * Resolved primary action for the current route. Resolution order:\n\t\t * 1. The current route's matching `pages[].primaryAction`\n\t\t * 2. `manifest.nav.primaryAction` as app-wide default\n\t\t * 3. `null` — no primary-action button renders\n\t\t *\n\t\t * Page-scoped declarations always win over the nav-root default.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactivePrimaryAction() {\n\t\t\tconst pages = this.effectiveManifest?.pages ?? []\n\t\t\tconst routeName = this.$route?.name\n\t\t\tif (routeName) {\n\t\t\t\tconst page = pages.find((p) => p.id === routeName)\n\t\t\t\tif (page && page.primaryAction) {\n\t\t\t\t\treturn page.primaryAction\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.effectiveManifest?.nav?.primaryAction ?? null\n\t\t},\n\n\t\t/**\n\t\t * MDI icon component for the resolved primary action. Honors the\n\t\t * action's `icon` field when set; falls back to the canonical\n\t\t * `Plus` icon to match NC's NcAppNavigationNew default styling.\n\t\t *\n\t\t * @return {import('vue').Component}\n\t\t */\n\t\tprimaryActionIconComponent() {\n\t\t\treturn this.mdiIconComponent(this.activePrimaryAction) ?? Plus\n\t\t},\n\n\t\t/**\n\t\t * Link props for the primary action's button: an `href` opens in a\n\t\t * new tab, a `route` is a router link. Null when the action is\n\t\t * neither (or there is no router), so it stays a plain button.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tprimaryActionLink() {\n\t\t\tconst action = this.activePrimaryAction\n\t\t\tif (!action) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (action.href) {\n\t\t\t\treturn { href: action.href, target: '_blank' }\n\t\t\t}\n\t\t\tif (action.route && this.$router) {\n\t\t\t\treturn { to: { name: action.route } }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * All visible items (filtered by permission and visibleIf conditions,\n\t\t * sorted by order). Retained for backwards-compat with the previous\n\t\t * public API and tests that read this computed; new code should use\n\t\t * `mainItems` / `settingsItems` instead.\n\t\t */\n\t\tvisibleItems() {\n\t\t\treturn (this.effectiveManifest?.menu ?? [])\n\t\t\t\t.filter((item) => this.passesPermission(item) && this.passesVisibleIf(item))\n\t\t\t\t.slice()\n\t\t\t\t.sort(byManifestOrder)\n\t\t},\n\n\t\t/**\n\t\t * Items that render in the top list (default placement).\n\t\t *\n\t\t * Note the `=== 'main'` rather than a negated list: an entry whose\n\t\t * section this component does not render must fall through to\n\t\t * NOTHING, not to the main list. `section: \"integrations\"` is\n\t\t * exactly that case — those entries render in CnAppRoot's\n\t\t * per-user settings modal, not in the navigation (ADR-110) — and a\n\t\t * `!== 'footer' && !== 'settings'` test would have quietly put\n\t\t * every one of them back in the nav this contract removes them\n\t\t * from.\n\t\t */\n\t\tmainItems() {\n\t\t\treturn this.visibleItems.filter((item) => (item.section ?? 'main') === 'main')\n\t\t},\n\n\t\t/**\n\t\t * Items pinned to the bottom of the navigation (section:\n\t\t * \"footer\") — rendered as flat NcAppNavigationItems above the\n\t\t * settings foldout. For always-visible, non-settings entries:\n\t\t * Documentation, Features & Roadmap, About.\n\t\t */\n\t\tfooterItems() {\n\t\t\treturn this.visibleItems.filter((item) => item.section === 'footer')\n\t\t},\n\n\t\t/**\n\t\t * Items that render INSIDE the NcAppNavigationSettings foldout\n\t\t * (section: \"settings\"). The foldout is the NC-native gear-icon\n\t\t * button that slides a panel open; these entries are app-level\n\t\t * configuration pages (Forms, Pipelines, Automations, …).\n\t\t */\n\t\tsettingsItems() {\n\t\t\treturn this.visibleItems.filter((item) => item.section === 'settings')\n\t\t},\n\n\t\t/**\n\t\t * Whether the settings foldout mounts. Mounts when there is at\n\t\t * least one `section: \"settings\"` item OR personal settings is\n\t\t * enabled — so every app shows a Settings gear with at least the\n\t\t * auto-prepended \"Personal settings\" entry. Only fully suppressed\n\t\t * when there are no settings items AND `nav.includePersonalSettings`\n\t\t * is explicitly `false`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowSettingsFoldout() {\n\t\t\treturn this.settingsItems.length > 0 || this.includePersonalSettings\n\t\t\t\t|| this.roadmapEntry !== null || this.documentationEntry !== null\n\t\t},\n\n\t\t/**\n\t\t * Whether to auto-prepend the \"Personal settings\" entry at the top\n\t\t * of the foldout. On by default; opt out with\n\t\t * `manifest.nav.includePersonalSettings: false` (e.g. when the app\n\t\t * has no per-user NcAppSettingsDialog wired through\n\t\t * cnOpenUserSettings).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tincludePersonalSettings() {\n\t\t\treturn this.effectiveManifest?.nav?.includePersonalSettings !== false\n\t\t},\n\n\t\t/**\n\t\t * Label for the foldout's gear button. Manifest override:\n\t\t * `nav.settingsLabel`; defaults to \"Settings\".\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsettingsFoldoutLabel() {\n\t\t\tconst custom = this.effectiveManifest?.nav?.settingsLabel\n\t\t\tif (typeof custom === 'string' && custom.length > 0) {\n\t\t\t\treturn this.effectiveTranslate(custom)\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Settings')\n\t\t},\n\n\t\t/**\n\t\t * Label for the auto-prepended Personal-settings entry.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tpersonalSettingsLabel() {\n\t\t\treturn t('nextcloud-vue', 'Personal settings')\n\t\t},\n\n\t\t/**\n\t\t * Optional \"Features & roadmap\" foldout entry. Enabled via\n\t\t * `nav.includeRoadmap`; `nav.roadmapUrl` is treated as an external link\n\t\t * when it looks like a URL, otherwise as an in-app router target.\n\t\t *\n\t\t * @return {{label: string, to: (string|null), href: (string|null)}|null}\n\t\t */\n\t\troadmapEntry() {\n\t\t\tconst nav = this.effectiveManifest?.nav\n\t\t\tif (!nav || nav.includeRoadmap !== true) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = (typeof nav.roadmapLabel === 'string' && nav.roadmapLabel)\n\t\t\t\t? this.effectiveTranslate(nav.roadmapLabel)\n\t\t\t\t: t('nextcloud-vue', 'Features & roadmap')\n\t\t\tconst target = typeof nav.roadmapUrl === 'string' ? nav.roadmapUrl.trim() : ''\n\t\t\tconst external = /^(https?:)?\\/\\//.test(target)\n\t\t\treturn { label, to: (target && !external) ? target : null, href: external ? target : null }\n\t\t},\n\n\t\t/**\n\t\t * Optional \"Documentation\" foldout entry. Enabled via\n\t\t * `nav.includeDocumentation`; always an external link (`nav.documentationUrl`).\n\t\t *\n\t\t * @return {{label: string, href: string}|null}\n\t\t */\n\t\tdocumentationEntry() {\n\t\t\tconst nav = this.effectiveManifest?.nav\n\t\t\tif (!nav || nav.includeDocumentation !== true) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst target = typeof nav.documentationUrl === 'string' ? nav.documentationUrl.trim() : ''\n\t\t\tif (!target) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = (typeof nav.documentationLabel === 'string' && nav.documentationLabel)\n\t\t\t\t? this.effectiveTranslate(nav.documentationLabel)\n\t\t\t\t: t('nextcloud-vue', 'Documentation')\n\t\t\treturn { label, href: target }\n\t\t},\n\n\t\t/**\n\t\t * Label for the auto-prepended Admin-settings entry.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tadminSettingsLabel() {\n\t\t\treturn t('nextcloud-vue', 'Admin settings')\n\t\t},\n\n\t\t/**\n\t\t * Accessible name (and hover tooltip) of the open-in-new marker on\n\t\t * the Admin-settings entry — the visual cue that the link leaves\n\t\t * the app for Nextcloud's own settings area.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\topensInNewTabHint() {\n\t\t\treturn t('nextcloud-vue', 'Opens in a new tab')\n\t\t},\n\n\t\t/**\n\t\t * Resolved app id for the Admin-settings link — the explicit\n\t\t * `appId` prop, else the `cnAppId` provided by CnAppRoot.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\teffectiveAppId() {\n\t\t\tconst id = this.appId || this.cnAppId\n\t\t\treturn (typeof id === 'string' && id.trim()) ? id.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * Target of the Admin-settings entry: the app's section in\n\t\t * Nextcloud's own admin settings (ADR-079 §1). This is a LINK, not\n\t\t * a modal — the app never re-implements the settings surface, and\n\t\t * the destination is authorized server-side by the settings\n\t\t * framework.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tadminSettingsHref() {\n\t\t\tif (!this.effectiveAppId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\t// ABSOLUTE (same-origin) on purpose: NcAppNavigationItem renders\n\t\t\t// target=\"_blank\" only for hrefs its isExternal() deems external\n\t\t\t// (scheme-prefixed) and ignores a passed `target` — a relative\n\t\t\t// path therefore always opens in the SAME tab. The absolute form\n\t\t\t// gets the new tab while keeping native anchor semantics\n\t\t\t// (middle-click, copy link).\n\t\t\tconst path = generateUrl('/settings/admin/{appId}', { appId: this.effectiveAppId })\n\t\t\tconst origin = (typeof window !== 'undefined' && window.location && window.location.origin) || ''\n\t\t\treturn origin + path\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the Admin-settings link: the user administers the\n\t\t * instance AND we can build a target. Visibility only — see the\n\t\t * `isAdmin` prop docs; the access check lives in Nextcloud.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowAdminSettingsLink() {\n\t\t\treturn this.isAdmin === true && this.adminSettingsHref !== ''\n\t\t},\n\n\t\t/**\n\t\t * Route name of the menu item that best matches the current route.\n\t\t * A direct match (current route name IS a menu target) wins;\n\t\t * otherwise the current path is matched against each item's page\n\t\t * path and the LONGEST prefix wins, so detail / nested routes (e.g.\n\t\t * `/expenses/:id`) light up — and auto-expand — their index entry\n\t\t * even though the detail route name is not in the menu. Longest\n\t\t * prefix disambiguates nested namespaces (e.g. `/pos` vs\n\t\t * `/pos/refunds`). Returns null when nothing matches.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tactiveRouteName() {\n\t\t\tconst routeName = this.$route?.name\n\t\t\tconst path = this.$route?.path\n\t\t\tconst flat = []\n\t\t\tfor (const item of this.visibleItems) {\n\t\t\t\tflat.push(item)\n\t\t\t\tfor (const child of this.visibleChildren(item)) {\n\t\t\t\t\tflat.push(child)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (routeName && flat.some((it) => it.route === routeName)) {\n\t\t\t\treturn routeName\n\t\t\t}\n\t\t\tif (!path) {\n\t\t\t\treturn routeName ?? null\n\t\t\t}\n\t\t\tlet best = null\n\t\t\tlet bestLen = -1\n\t\t\tfor (const it of flat) {\n\t\t\t\tif (!it.route) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst pagePath = this.pageForItem(it)?.route\n\t\t\t\tif (!pagePath || pagePath === '/' || pagePath.includes(':')) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (path === pagePath || path.startsWith(pagePath + '/')) {\n\t\t\t\t\tif (pagePath.length > bestLen) {\n\t\t\t\t\t\tbest = it.route\n\t\t\t\t\t\tbestLen = pagePath.length\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn best ?? routeName ?? null\n\t\t},\n\n\t\t/**\n\t\t * Every visible entry and child, flat, in menu order.\n\t\t *\n\t\t * @return {Array<object>} The entries.\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t */\n\t\tflatEntries() {\n\t\t\tconst flat = []\n\t\t\tfor (const item of this.visibleItems) {\n\t\t\t\tflat.push(item, ...this.visibleChildren(item))\n\t\t\t}\n\t\t\treturn flat\n\t\t},\n\n\t\t/**\n\t\t * The entry the query rule marks while the reader is on a menu\n\t\t * route itself (not on a page below it). Watched, so the nav can\n\t\t * remember which entry of a route the reader last used.\n\t\t *\n\t\t * @return {object|null} The entry, or null on a page below a route.\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t */\n\t\tentryActiveOnOwnRoute() {\n\t\t\tconst routeName = this.$route?.name\n\t\t\tif (!routeName || routeName !== this.activeRouteName) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn this.flatEntries.find((entry) => this.isActiveByRule(entry)) ?? null\n\t\t},\n\n\t\t/**\n\t\t * The one entry to mark on a page BELOW a menu route when the query\n\t\t * rule marks none.\n\t\t *\n\t\t * On a detail page (`/cases/123`) the active route is found by path\n\t\t * prefix, and the address carries the detail page's query, not the\n\t\t * list's. When every entry on that list has a `query` (\"My work\",\n\t\t * \"Queue\"), none of them matched and the menu showed no place at\n\t\t * all. Marking all of them is the defect the query rule ended, so\n\t\t * this picks exactly one: the entry the reader last had active on\n\t\t * that route, else the first in menu order.\n\t\t *\n\t\t * Null whenever the existing rule already marks an entry, so a list\n\t\t * with an entry without a `query` behaves as it did.\n\t\t *\n\t\t * @return {object|null} The entry to mark, or null.\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t */\n\t\tsubRouteParent() {\n\t\t\tconst active = this.activeRouteName\n\t\t\tif (!active || this.$route?.name === active) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst entries = this.flatEntries.filter((entry) => !entry.href && entry.route === active)\n\t\t\tif (entries.length === 0 || entries.some((entry) => this.isActiveByRule(entry))) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst remembered = this.lastActiveEntryByRoute[active]\n\t\t\treturn entries.find((entry) => this.entryKey(entry) === remembered) ?? entries[0]\n\t\t},\n\t},\n\n\twatch: {\n\t\tactiveRouteName: {\n\t\t\timmediate: true,\n\t\t\thandler(_current, previous) {\n\t\t\t\tthis.pinGroupsEntered(previous)\n\t\t\t},\n\t\t},\n\n\t\t// The manifest can arrive after the route did.\n\t\tvisibleItems() {\n\t\t\tthis.pinGroupsEntered(this.activeRouteName)\n\t\t},\n\n\t\tentryActiveOnOwnRoute: {\n\t\t\timmediate: true,\n\t\t\t/**\n\t\t\t * Remember which entry of a route the reader used, for\n\t\t\t * `subRouteParent`.\n\t\t\t *\n\t\t\t * @param {object|null} entry The entry active on its own route.\n\t\t\t * @return {void}\n\t\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t\t */\n\t\t\thandler(entry) {\n\t\t\t\tif (entry && entry.route) {\n\t\t\t\t\tthis.lastActiveEntryByRoute[entry.route] = this.entryKey(entry)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Non-reactive one-shot latch for warnAutoCountMisconfigured(). It has\n\t\t// to be seeded here rather than lazily on first use: `resolveCount()`\n\t\t// runs DURING RENDER, and Vue 3's instance proxy emits\n\t\t// \"Property \"_autoCountWarned\" was accessed during render but is not\n\t\t// defined on instance\" for the first read of an unset instance field\n\t\t// (Vue 2 read it back as plain `undefined`, silently). It deliberately\n\t\t// stays out of `data()`: a reactive Set mutated inside render would\n\t\t// re-trigger the render effect.\n\t\tthis._autoCountWarned = new Set()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve a menu item's `icon` string to an MDI Vue component. MDI names\n\t\t * resolve via the per-app `registerIcons()` registry; legacy Nextcloud\n\t\t * `icon-*` class names resolve via the shared `cssIconBridge` so they\n\t\t * render monochrome (fill:currentColor) like every other glyph.\n\t\t * Returns `null` (→ the `:icon=\"cssIconClass(item)\"` CSS-class fallback)\n\t\t * for unbridged `icon-*` names and unknown MDI names.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {import('vue').Component|null}\n\t\t */\n\t\tmdiIconComponent(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (icon.startsWith('icon-')) {\n\t\t\t\treturn bridgedMdiForCssIcon(icon) || null\n\t\t\t}\n\t\t\t// ADR-077: the app's own registerIcons() entries win (so an app can\n\t\t\t// override), then the shared semantic vocabulary — which resolves\n\t\t\t// WITHOUT the app having registered anything. Before the vocabulary\n\t\t\t// existed, a name the app forgot to register resolved to null here\n\t\t\t// and the entry rendered with no icon at all.\n\t\t\treturn ICON_MAP[icon] || getSemanticIconComponent(icon) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the item declares an icon that NOTHING can resolve — not the\n\t\t * app registry, not the semantic vocabulary, not the widget registry, and\n\t\t * not a URL / SVG path.\n\t\t *\n\t\t * ADR-077 rule 4: such an icon MUST degrade to a visible fallback glyph.\n\t\t * Historically it rendered as empty space, which is indistinguishable\n\t\t * from a deliberate no-icon design choice — 29 of hrmq's 72 nav entries\n\t\t * were silently blank this way. A visible \"?\" is discoverable; blank is\n\t\t * not.\n\t\t *\n\t\t * `icon-*` values are excluded: those have their own CSS-class fallback\n\t\t * path via the `cssIconClass` method.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {boolean} true when the declared icon resolves to nothing.\n\t\t */\n\t\tisUnresolvedIcon(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (icon.startsWith('icon-')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !this.mdiIconComponent(item)\n\t\t\t\t&& !this.isRichIcon(item)\n\t\t\t\t&& !this.isRegistryIcon(item)\n\t\t},\n\n\t\t/**\n\t\t * Whether the item's icon is a raw SVG path or an image URL (incl. the\n\t\t * `data:` URIs the bundled NL-government sets emit) — neither of which is a\n\t\t * component, so `ICON_MAP` can't resolve it and the `#icon` slot must\n\t\t * render it through CnMenuItemIcon instead.\n\t\t *\n\t\t * Without this, an icon picked from CnIconBrowser's Gemeente / Den Haag /\n\t\t * RVO tabs would simply not appear in the navigation.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {boolean} true for path/URL icons.\n\t\t */\n\t\tisRichIcon(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0 || icon.startsWith('icon-')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn isCustomIconUrl(icon) || isSvgPath(icon)\n\t\t},\n\n\t\t/**\n\t\t * Whether the item's icon is a plain MDI *name* that the shared widget-icon\n\t\t * registry can render (e.g. \"Heart\", \"Home\" — what CnIconBrowser emits from\n\t\t * its component-name sources).\n\t\t *\n\t\t * `ICON_MAP` only holds what the consuming app passed to `registerIcons()`,\n\t\t * and an app rendering USER-AUTHORED manifests cannot pre-register whatever\n\t\t * icon a user might pick — Buildiq registers none at all. So a picked name\n\t\t * failed `mdiIconComponent` (not registered) AND `isRichIcon` (not a URL or\n\t\t * SVG path), the `#icon` slot was skipped entirely, and the menu item rendered\n\t\t * with NO icon — even though CnMenuItemIcon → CnWidgetIcon could resolve it.\n\t\t *\n\t\t * Gate on the registry actually HAVING the name: `getIconComponent` answers\n\t\t * the default icon for anything unknown, so gating on it would render a wrong\n\t\t * (but plausible) icon for a typo instead of none.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {boolean} true when the widget-icon registry can render this name.\n\t\t */\n\t\tisRegistryIcon(item) {\n\t\t\treturn hasRegistryIcon(item?.icon)\n\t\t},\n\n\t\t/**\n\t\t * Pass-through for the `:icon` prop on NcAppNavigationItem when\n\t\t * the manifest declares a Nextcloud CSS-class icon (`icon-*`).\n\t\t * Returns an empty string when the icon is an MDI name OR a bridged\n\t\t * `icon-*` (rendered via the `#icon` slot instead), so NcAppNavigationItem\n\t\t * doesn't also paint a CSS-class background-image.\n\t\t *\n\t\t * @param {{ icon?: string }} item Menu item descriptor.\n\t\t * @return {string}\n\t\t */\n\t\tcssIconClass(item) {\n\t\t\tconst icon = item?.icon\n\t\t\tif (typeof icon !== 'string' || icon.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (!icon.startsWith('icon-')) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn bridgedMdiForCssIcon(icon) ? '' : icon\n\t\t},\n\n\t\tpassesPermission(item) {\n\t\t\tif (!item.permission) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (!this.permissions || this.permissions.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.permissions.includes(item.permission)\n\t\t},\n\n\t\t/**\n\t\t * Evaluate a menu item's `visibleIf` condition block.\n\t\t *\n\t\t * Returns `true` (visible) when:\n\t\t * - No `visibleIf` is declared (backwards-compatible default).\n\t\t * - `visibleIf.appInstalled` is set AND the named app is\n\t\t * installed / enabled (checked via `OC.appswebroots` then the\n\t\t * capabilities fallback, cached per page load by `isAppInstalled`).\n\t\t * - Context-path predicates (any key that is a dot-separated path\n\t\t * into `manifest.runtime`) all pass against the current runtime\n\t\t * data. Predicates are evaluated by `passesContextPredicates`.\n\t\t * Example: `{ \"user.primaryRole\": { \"in\": [\"hr\", \"compliance\"] } }`\n\t\t * hides the entry unless `manifest.runtime.user.primaryRole` is\n\t\t * `\"hr\"` or `\"compliance\"`. When the runtime block is absent the\n\t\t * entry is hidden (fail-safe: never show role-gated items to\n\t\t * unidentified users).\n\t\t *\n\t\t * All conditions are combined with implicit AND — every condition\n\t\t * must pass for the item to render. Returns `false` when any fails.\n\t\t *\n\t\t * @param {object} item Menu item (or child) to evaluate.\n\t\t * @return {boolean} Whether the item should render.\n\t\t */\n\t\tpassesVisibleIf(item) {\n\t\t\tconst condition = item.visibleIf\n\t\t\tif (!condition || typeof condition !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// Specialised condition: appInstalled.\n\t\t\tif (condition.appInstalled) {\n\t\t\t\tif (!isAppInstalled(condition.appInstalled)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Context-path predicates: any non-reserved key is a dot-path\n\t\t\t// into manifest.runtime evaluated by passesContextPredicates.\n\t\t\tconst runtime = this.effectiveManifest?.runtime ?? null\n\t\t\tif (!passesContextPredicates(condition, runtime)) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * A group's visible children, in `order`.\n\t\t *\n\t\t * The SORT is the fix for a silent no-op: this used to filter only, so\n\t\t * `order` on a child was accepted, documented and ignored. It bites\n\t\t * hardest on relocated leaves, because `applyMenuRelocations()` walks\n\t\t * the menu backwards and appends each move, which lands a group's\n\t\t * children in REVERSE manifest order — an order nobody wrote down and\n\t\t * no app can influence except by reordering `manifest.json` itself.\n\t\t * Pipelinq's Customer Support group read \"Projecten, My Work, Queue,\n\t\t * Tasks, All tickets\" against declared orders of 55, 20, 10, 60, 50.\n\t\t *\n\t\t * Same comparator as the top level, so one rule governs both: items\n\t\t * without an `order` render last, in their existing relative order\n\t\t * (Array.prototype.sort is stable).\n\t\t *\n\t\t * @param {object} item The group whose children to render.\n\t\t * @return {Array<object>} Visible children, ordered.\n\t\t */\n\t\tvisibleChildren(item) {\n\t\t\tif (!Array.isArray(item.children)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn item.children\n\t\t\t\t.filter((c) => this.passesPermission(c) && this.passesVisibleIf(c))\n\t\t\t\t.sort(byManifestOrder)\n\t\t},\n\n\t\tresolveLabel(item) {\n\t\t\treturn this.effectiveTranslate(item.label)\n\t\t},\n\n\t\t/**\n\t\t * Whether a menu entry is the page the reader is on.\n\t\t *\n\t\t * Two entries may share a route and differ only in `query` (\"My\n\t\t * work\" and \"Queue\" on one list). The route name alone lit both. An\n\t\t * entry with a `query` is active only when the address carries that\n\t\t * query; an entry without one steps back when a sibling's query\n\t\t * matches, so exactly one of them is marked.\n\t\t *\n\t\t * On a page below a list where that rule marks nothing, the one\n\t\t * entry `subRouteParent` picks is active instead.\n\t\t *\n\t\t * @param {object} item Menu entry.\n\t\t * @return {boolean} True when the entry is the current page.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t */\n\t\tisActive(item) {\n\t\t\tif (this.isActiveByRule(item)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst parent = this.subRouteParent\n\t\t\treturn parent !== null && toRaw(parent) === toRaw(item)\n\t\t},\n\n\t\t/**\n\t\t * The route and query rule on its own, without the fallback for a\n\t\t * page below a list. `subRouteParent` asks this to see whether the\n\t\t * rule already marks an entry.\n\t\t *\n\t\t * @param {object} item Menu entry.\n\t\t * @return {boolean} True when the rule marks the entry.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t */\n\t\tisActiveByRule(item) {\n\t\t\tif (item.href || !item.route) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (item.route !== this.activeRouteName) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (item.query && typeof item.query === 'object') {\n\t\t\t\treturn this.queryMatches(item)\n\t\t\t}\n\t\t\tconst siblings = []\n\t\t\tfor (const entry of this.visibleItems) {\n\t\t\t\tsiblings.push(entry, ...this.visibleChildren(entry))\n\t\t\t}\n\t\t\treturn !siblings.some((entry) => entry !== item\n\t\t\t\t&& entry.route === item.route\n\t\t\t\t&& entry.query && typeof entry.query === 'object'\n\t\t\t\t&& this.queryMatches(entry))\n\t\t},\n\n\t\t/**\n\t\t * What an entry is remembered by: its `id`, else its label.\n\t\t *\n\t\t * @param {object} item Menu entry.\n\t\t * @return {string} The key.\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry\n\t\t */\n\t\tentryKey(item) {\n\t\t\treturn String(item.id ?? item.label ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the address carries every key of an entry's `query`.\n\t\t * Values are compared as strings, since an address has no numbers.\n\t\t *\n\t\t * @param {object} item Menu entry with a `query`.\n\t\t * @return {boolean} True when every declared key matches.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query\n\t\t */\n\t\tqueryMatches(item) {\n\t\t\tconst current = this.$route?.query ?? {}\n\t\t\tconst keys = Object.keys(item.query)\n\t\t\treturn keys.length > 0 && keys.every((key) => String(current[key] ?? '') === String(item.query[key]))\n\t\t},\n\n\t\t/**\n\t\t * Whether a menu entry renders as a `NcAppNavigationCaption`\n\t\t * (`type: \"caption\"`) rather than a clickable\n\t\t * `NcAppNavigationItem`. Caption entries ignore `route`, `href`,\n\t\t * `action`, `icon`, `count`, `children`, and `pinned`.\n\t\t *\n\t\t * @param {{ type?: string }} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\tisCaption(item) {\n\t\t\treturn item?.type === 'caption'\n\t\t},\n\n\t\t/**\n\t\t * Whether the host has registered a scoped slot named\n\t\t * `item-${id}-actions` for this menu item. Used to gate template\n\t\t * rendering of the per-item `#actions` slot pass-through so items\n\t\t * without a matching slot don't render an empty `<template #actions>`.\n\t\t *\n\t\t * @param {{ id: string }} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\thasItemActionsSlot(item) {\n\t\t\tif (!item?.id) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst name = `item-${item.id}-actions`\n\t\t\treturn Boolean((this.$slots && this.$slots[name])\n\t\t\t\t|| (this.$slots && this.$slots[name]))\n\t\t},\n\n\t\t/**\n\t\t * Resolve the count value to render in this entry's\n\t\t * `NcCounterBubble` (in the `#counter` slot of\n\t\t * `NcAppNavigationItem`).\n\t\t *\n\t\t * - Literal positive integer in `item.count` → return as-is.\n\t\t * - `item.count === \"auto\"` → look up the entry's resolved page;\n\t\t * when that page is `type: \"index\"` with a `register`/`schema`\n\t\t * in its `config`, return\n\t\t * `cnMenuCounts[register][schema] ?? null`.\n\t\t *\n\t\t * Returns `null` (no badge) when:\n\t\t * - `item.count` is unset or falsy\n\t\t * - The literal value is `0`\n\t\t * - `count === \"auto\"` but no matching index page resolves\n\t\t * (also emits a one-shot `console.warn`)\n\t\t * - The store has no entry for the resolved `(register, schema)`\n\t\t *\n\t\t * @param {object} item Menu entry descriptor.\n\t\t * @return {number|null} Count to render, or null for no badge.\n\t\t */\n\t\tresolveCount(item) {\n\t\t\tconst raw = item?.count\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof raw === 'number') {\n\t\t\t\treturn raw > 0 ? raw : null\n\t\t\t}\n\t\t\tif (raw !== 'auto') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst page = this.pageForItem(item)\n\t\t\tconst register = page?.config?.register\n\t\t\tconst schema = page?.config?.schema\n\t\t\tif (page?.type !== 'index' || !register || !schema) {\n\t\t\t\tthis.warnAutoCountMisconfigured(item)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst value = this.cnMenuCounts?.[register]?.[schema]\n\t\t\tif (typeof value !== 'number' || value <= 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * One-shot `console.warn` per menu-item id for misconfigured\n\t\t * `count: \"auto\"` entries (no resolvable index page). Keeps the\n\t\t * console quiet on re-renders.\n\t\t *\n\t\t * @param {{ id: string }} item Menu entry descriptor.\n\t\t * @return {void}\n\t\t * @private\n\t\t */\n\t\twarnAutoCountMisconfigured(item) {\n\t\t\tif (this._autoCountWarned.has(item.id)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis._autoCountWarned.add(item.id)\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn(`[CnAppNav] Menu entry \"${item.id}\" declares count: \"auto\" but has no resolvable index-type page with register + schema config — no badge will render.`)\n\t\t},\n\n\t\t/**\n\t\t * Look up an item's resolved page (`pages[]` entry whose `id`\n\t\t * matches the menu item's `route`) — used to decide whether the\n\t\t * NcAppNavigationItem should match its router-link `exact`.\n\t\t *\n\t\t * @param {object} item Menu item to resolve.\n\t\t * @return {object|null} Matching page entry, or null when the item\n\t\t * has no `route` or no page matches.\n\t\t */\n\t\tpageForItem(item) {\n\t\t\tif (!item.route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst pages = this.effectiveManifest?.pages ?? []\n\t\t\treturn pages.find((p) => p.id === item.route) ?? null\n\t\t},\n\n\t\t/**\n\t\t * Build the `:to` value for an `NcAppNavigationItem`. Action\n\t\t * items (`action: \"user-settings\"`) and `href` items return\n\t\t * `null` so the entry is NOT a vue-router link: action items\n\t\t * fall through to the click handler, `href` items render a real\n\t\t * anchor via `itemHref`. Route items return a named route.\n\t\t *\n\t\t * @param {object} item Menu item being rendered.\n\t\t * @return {object|null} A `{ name }` route object, or null for\n\t\t * action / href / route-less items.\n\t\t */\n\t\titemTo(item) {\n\t\t\tif (item.action) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (item.href) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (!item.route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\t// Carry optional query params so a nav entry can deep-link to a\n\t\t\t// pre-filtered index page (e.g. one entry per case type → Cases?caseType=…).\n\t\t\treturn item.query ? { name: item.route, query: item.query } : { name: item.route }\n\t\t},\n\n\t\t/**\n\t\t * Build the `:href` value for an `NcAppNavigationItem`. Returns\n\t\t * the item's `href` so the entry renders as a real anchor whose\n\t\t * destination is visible on hover and which gets the native link\n\t\t * cursor. `NcAppNavigationItem` adds `target=\"_blank\"` itself for\n\t\t * external (`scheme://`) URLs, so those open in a new tab while\n\t\t * internal app paths (e.g. `/index.php/apps/foo/`) navigate in the\n\t\t * same tab — no `window.open` interception. The `admin-settings`\n\t\t * action links to `adminSettingsHref` (absolute, so it opens in a\n\t\t * new tab); other action items carry no href. Returns `null` for\n\t\t * non-href items so the entry stays a router-link / button.\n\t\t *\n\t\t * @param {object} item Menu item being rendered.\n\t\t * @return {string|null} The destination URL, or null.\n\t\t */\n\t\titemHref(item) {\n\t\t\tif (item.action === 'admin-settings') {\n\t\t\t\treturn this.adminSettingsHref || null\n\t\t\t}\n\t\t\tif (item.action) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn item.href || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a menu group renders expanded. Local `openState` (set\n\t\t * by the chevron, a title click, or `pinGroupsEntered`) wins;\n\t\t * otherwise the group auto-expands when it contains the active\n\t\t * route — so deep-linking to a child page reveals which group it\n\t\t * lives in — and finally falls back to the manifest's `item.open`\n\t\t * for the initial render.\n\t\t *\n\t\t * @param {{ id: string, open?: boolean }} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\tisItemOpen(item) {\n\t\t\tconst local = this.openState[item.id]\n\t\t\tif (local !== undefined) {\n\t\t\t\treturn local\n\t\t\t}\n\t\t\tif (this.hasActiveChild(item)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Boolean(item.open)\n\t\t},\n\n\t\t/**\n\t\t * Whether any of a group's visible children is the active route.\n\t\t * Drives auto-expansion of the parent group on page load.\n\t\t *\n\t\t * @param {object} item Menu entry descriptor.\n\t\t * @return {boolean}\n\t\t */\n\t\thasActiveChild(item) {\n\t\t\treturn this.visibleChildren(item).some((child) => this.isActive(child))\n\t\t},\n\n\t\t/**\n\t\t * Record a group's expand/collapse state. Bound to\n\t\t * NcAppNavigationItem's `@update:open` (chevron clicks) and\n\t\t * called from `onItemClick` for title clicks on route-less\n\t\t * group items.\n\t\t *\n\t\t * @param {{ id: string }} item Menu entry descriptor.\n\t\t * @param {boolean} value New open state.\n\t\t */\n\t\tsetItemOpen(item, value) {\n\t\t\tthis.openState[item.id] = value\n\t\t},\n\n\t\t/**\n\t\t * Pin open every group the active route just entered.\n\t\t *\n\t\t * Auto-expansion used to be read off the route alone, so a group\n\t\t * opened for a deep link collapsed the moment the route left it. A\n\t\t * group opened for you stays open until you close it. Only a group\n\t\t * the route ENTERS is pinned: one you closed while on one of its\n\t\t * children stays closed while you move between them.\n\t\t *\n\t\t * @param {string|null} previousRoute The active route before this change, or undefined at first render.\n\t\t */\n\t\tpinGroupsEntered(previousRoute) {\n\t\t\tfor (const item of this.visibleItems) {\n\t\t\t\tconst children = this.visibleChildren(item)\n\t\t\t\tif (children.length === 0 || !this.hasActiveChild(item)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst wasInside = children.some((child) => child.route === previousRoute)\n\t\t\t\tif (!wasInside || this.openState[item.id] === undefined) {\n\t\t\t\t\tthis.openState[item.id] = true\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Click handler. Dispatch order: action keyword → group\n\t\t * open/toggle. For `action: \"user-settings\"` invokes the injected\n\t\t * `cnOpenUserSettings` (provided by CnAppRoot) and prevents\n\t\t * default; `action: \"admin-settings\"` is a real link to\n\t\t * `/settings/admin/<appId>` (see `itemHref`) and is left to the\n\t\t * browser; for `action:\n\t\t * \"replay-walkthrough\"` invokes the injected\n\t\t * `cnReplayWalkthrough(item.tourId)` and prevents default. `href`\n\t\t * items with no children are NOT handled here — they render a real\n\t\t * anchor via `itemHref`, so the browser navigates natively\n\t\t * (external URLs open in a new tab, internal app paths in the same\n\t\t * tab).\n\t\t *\n\t\t * A group's children visibility on click depends on whether the\n\t\t * group ITSELF has a destination:\n\t\t * - Route-less, href-less: a pure group header, so its anchor is\n\t\t * a dead `#` link and the click's only job is to TOGGLE the\n\t\t * children open/closed (same effect as the collapse chevron).\n\t\t * - Carries a `route` or `href`: the click is a real navigation\n\t\t * (handled natively by `:to` / the anchor, not prevented here),\n\t\t * and this additionally forces the group OPEN — never closed —\n\t\t * so the reader lands on the group's own page without a second\n\t\t * click to see what else it holds. The collapse chevron is the\n\t\t * only control that can close such a group again.\n\t\t *\n\t\t * @param {object} item Menu item being clicked.\n\t\t * @param {Event} [event] Native click event (used to call\n\t\t * preventDefault for action / group links).\n\t\t */\n\t\tonItemClick(item, event) {\n\t\t\tif (item.action === 'user-settings') {\n\t\t\t\tif (event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\tthis.cnOpenUserSettings()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (item.action === 'admin-settings') {\n\t\t\t\t// ADR-079: a real link to Nextcloud's own settings (see\n\t\t\t\t// itemHref), followed natively in a new tab. Without an app\n\t\t\t\t// id there is no href, and the click does nothing.\n\t\t\t\tif (!this.adminSettingsHref && event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (item.action === 'replay-walkthrough') {\n\t\t\t\tif (event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\tthis.cnReplayWalkthrough(item.tourId)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.visibleChildren(item).length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!item.route && !item.href) {\n\t\t\t\tif (event && typeof event.preventDefault === 'function') {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t}\n\t\t\t\tthis.setItemOpen(item, !this.isItemOpen(item))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The item is ALSO a real destination: let the click navigate\n\t\t\t// natively and reveal the children alongside it.\n\t\t\tthis.setItemOpen(item, true)\n\t\t},\n\n\t\t/**\n\t\t * Click handler for the auto-prepended Personal-settings entry in\n\t\t * the settings foldout. Invokes the injected `cnOpenUserSettings`\n\t\t * (provided by CnAppRoot → opens the host's NcAppSettingsDialog).\n\t\t * No-op inject when mounted standalone.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonPersonalSettingsClick() {\n\t\t\tthis.cnOpenUserSettings()\n\t\t},\n\n\t\t/**\n\t\t * Click handler for the manifest-declared primary action. Emits\n\t\t * `@primary-action` AND the back-compat `@primary-action-click`\n\t\t * event for host-side handling. Navigation is the link's own: an\n\t\t * `href` or `route` action renders as a real link (see\n\t\t * `primaryActionLink`), so the browser or router follows it after\n\t\t * these events. Not invoked when the host overrides the\n\t\t * `#primary-action` slot (it provides its own handler).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonPrimaryActionClick() {\n\t\t\tconst action = this.activePrimaryAction\n\t\t\tif (!action) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst payload = {\n\t\t\t\tid: action.id,\n\t\t\t\tlabel: action.label,\n\t\t\t\ticon: action.icon,\n\t\t\t\troute: action.route,\n\t\t\t\thref: action.href,\n\t\t\t\tpayload: action.payload,\n\t\t\t\tpage: this.$route?.name,\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event primary-action Emitted when the resolved primary-action button is clicked. Payload includes the action descriptor (`{ id, label, icon, route, href, payload }`) plus the current `page` (route name) for host dispatchers.\n\t\t\t * @type {{ id?: string, label: string, icon?: string, route?: string, href?: string, payload?: unknown, page?: string }}\n\t\t\t */\n\t\t\tthis.$emit('primary-action', payload)\n\t\t\t/**\n\t\t\t * @event primary-action-click Back-compat alias for `@primary-action`. Payload is the resolved primary action object as declared in the manifest.\n\t\t\t */\n\t\t\tthis.$emit('primary-action-click', action)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* The brand block at the top of the navigation. */\n.cn-app-nav__brand {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: calc(2.5 * var(--default-grid-baseline));\n\tmin-width: 0;\n\tpadding: calc(2 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline)) calc(3 * var(--default-grid-baseline));\n}\n\n.cn-app-nav__brand-logo {\n\tflex: none;\n\theight: 34px;\n\tmax-width: 50%;\n\tobject-fit: contain;\n\twidth: auto;\n}\n\n.cn-app-nav__brand-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tline-height: 1.15;\n\tmin-width: 0;\n}\n\n.cn-app-nav__brand-name {\n\tcolor: var(--color-main-text);\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n\toverflow-wrap: anywhere;\n}\n\n.cn-app-nav__brand-caption {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\toverflow-wrap: anywhere;\n}\n\n/*\n * The legacy `icon-*` classes in Nextcloud render a background-image\n * with a hardcoded dark fill, so they stay grey when an entry becomes\n * active (text turns white against the primary-element background).\n * Force the icon to white in the active state to match the label.\n * Only applies when the icon is provided via the `icon` prop (CSS\n * class) — items using a `<template #icon>` MDI component already\n * inherit `currentColor`.\n */\n.app-navigation-entry.active .app-navigation-entry-icon[class*=\"icon-\"] {\n\tfilter: brightness(0) invert(1);\n}\n\n/* The link form of the primary action, matching NcAppNavigationNew's scoped layout. */\n.cn-app-nav__primary-action {\n\tdisplay: block;\n\tpadding: calc(var(--default-grid-baseline, 4px) * 2);\n}\n\n/*\n * Footer-section items (section: \"footer\") render in NcAppNavigation's\n * #footer slot, outside the scroll container, so they stay visible above\n * the settings foldout regardless of menu length. (The interim\n * `pinned`-prop approach kept them inside the scrollable list, where\n * `margin-top: auto` only bottom-pins while the list does not overflow —\n * long menus showed Documentation / Features & roadmap mid-scroll.)\n *\n * No `padding` here on purpose: it comes from NcAppNavigationList's own\n * `var(--app-navigation-padding)`, so it stays in step with the main list.\n *\n * As a direct `> ul` child of `.app-navigation__content`, NC makes it a\n * shrinkable, scrollable flex item, and NcAppNavigationList adds\n * `overflow-y: auto` besides. That let a two-entry list be squeezed below its\n * content and grow a scrollbar on a short menu, so opt out of both.\n */\n.cn-app-nav__footer-list {\n\tlist-style: none;\n\tmargin: 0;\n\tflex-shrink: 0 !important;\n\toverflow: visible !important;\n}\n\n/*\n * Align the settings foldout's items with the main/footer list items,\n * whose icons sit at a 16px inset. The foldout's items live in a bare\n * <ul> inside NcAppNavigationSettings' panel (`#app-settings`, padding\n * 3px), so without this they start ~5px further left and the icon\n * column looks ragged. (The native settings-toggle button sits ~1px\n * left of the 16px baseline, but NC owns that with an !important\n * shorthand — a sub-pixel difference we leave to the native component.)\n */\n.cn-app-nav__settings-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding-inline-start: 5px;\n}\n</style>\n"],"names":["_createBlock","_Fragment","_renderList","_openBlock","_createElementBlock","_resolveDynamicComponent","_createVNode","_renderSlot","_mergeProps","_toDisplayString"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { translate } from '@nextcloud/l10n';
1
2
  import { NcNoteCard } from '@nextcloud/vue';
2
3
  import { resolveHref, followLinkClick } from '../../utils/linkNavigation.js';
3
4
  import { safeHref } from '../../utils/safeHref.js';
@@ -43,6 +44,12 @@ const MAX_ACTIONS = 2;
43
44
  * condition evaluates true — a failed fetch never breaks (or spams) a
44
45
  * dashboard.
45
46
  *
47
+ * An attention card (`layout: "attention"`) whose `visibleWhen` request
48
+ * FAILS renders one quiet line instead, "Could not check", with the reason as
49
+ * a tooltip. A hidden card reads as "nothing needs attention", and a failed
50
+ * count does not know that. A request that succeeds with a value that does
51
+ * not meet the condition still renders nothing.
52
+ *
46
53
  * `layout: "attention"` turns the banner into an attention card: a card with
47
54
  * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line
48
55
  * and up to two `actions` (the first is the primary one unless an action says
@@ -184,7 +191,9 @@ var script = {
184
191
  * the banner renders from this verdict instead of fetching again —
185
192
  * no duplicate request and no hidden-until-self-evaluated flash —
186
193
  * and `value` feeds the `{value}` text placeholder. `null` (the
187
- * default) keeps the banner self-evaluating.
194
+ * default) keeps the banner self-evaluating. A host whose request
195
+ * failed passes `{ met: false, value: null, failed: true, reason }`,
196
+ * and an attention card then says it could not check.
188
197
  *
189
198
  * @type {object|null}
190
199
  */
@@ -205,6 +214,13 @@ var script = {
205
214
  'action',
206
215
  ],
207
216
 
217
+ /**
218
+ * The banner's own state: the evaluated condition, the value it read,
219
+ * and whether the request for it failed.
220
+ *
221
+ * @return {object} The state.
222
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
223
+ */
208
224
  data() {
209
225
  return {
210
226
  /** Id that ties the attention card's region to its title. */
@@ -217,6 +233,10 @@ var script = {
217
233
  * the text wherever it says `{value}`.
218
234
  */
219
235
  conditionValue: null,
236
+ /** Whether the visibleWhen request itself failed (not: answered "no"). */
237
+ conditionFailed: false,
238
+ /** Why it failed, as the request reported it. */
239
+ conditionFailureReason: '',
220
240
  }
221
241
  },
222
242
 
@@ -365,6 +385,52 @@ var script = {
365
385
  return this.conditionMet === true
366
386
  },
367
387
 
388
+ /**
389
+ * Whether to say the check failed: an attention card, with words to
390
+ * show, whose `visibleWhen` request failed. A plain banner stays
391
+ * hidden on failure, as it always did.
392
+ *
393
+ * @return {boolean}
394
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
395
+ */
396
+ checkFailed() {
397
+ const title = this.title || (this.content && this.content.title) || '';
398
+ return this.isAttention
399
+ && !!this.resolvedVisibleWhen
400
+ && this.conditionFailed
401
+ && (title !== '' || this.resolvedText !== '')
402
+ },
403
+
404
+ /**
405
+ * The one line shown when the check failed. It names the card by its
406
+ * title, unless the title needs the value that could not be read.
407
+ *
408
+ * @return {string}
409
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
410
+ */
411
+ uncheckedText() {
412
+ const title = this.title || (this.content && this.content.title) || this.resolvedText;
413
+ if (!title || title.includes('{value}')) {
414
+ return translate('nextcloud-vue', 'Could not check')
415
+ }
416
+ // `escape: false`: the result is rendered through `{{ }}`, which escapes.
417
+ return translate('nextcloud-vue', 'Could not check: {subject}', { subject: title }, undefined, { escape: false })
418
+ },
419
+
420
+ /**
421
+ * The reason the check failed, for the tooltip and for assistive
422
+ * technology.
423
+ *
424
+ * @return {string}
425
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
426
+ */
427
+ failureTooltip() {
428
+ if (!this.conditionFailureReason) {
429
+ return translate('nextcloud-vue', 'The check failed.')
430
+ }
431
+ return translate('nextcloud-vue', 'The check failed: {reason}', { reason: this.conditionFailureReason }, undefined, { escape: false })
432
+ },
433
+
368
434
  /** Whether the banner navigates on click (route set + router present). */
369
435
  clickable() {
370
436
  return !!this.resolvedRoute && !!this.$router
@@ -439,27 +505,36 @@ var script = {
439
505
  * modes — the Wave-1 banner shape is the canonical one, extracted to
440
506
  * `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),
441
507
  * keeping the read VALUE for the `{value}` text placeholder.
442
- * Fail-safe: any fetch/shape error leaves the banner hidden.
508
+ * Fail-safe: any fetch/shape error leaves the banner hidden. The
509
+ * failure is kept (`conditionFailed`, with its reason) so an
510
+ * attention card can say it could not check.
443
511
  *
444
512
  * @return {Promise<void>}
513
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
445
514
  */
446
515
  async evaluateCondition() {
447
516
  const outcome = this.resolvedConditionOutcome;
448
517
  if (outcome) {
449
518
  this.conditionMet = outcome.met === true;
450
519
  this.conditionValue = outcome.value !== undefined ? outcome.value : null;
520
+ this.conditionFailed = outcome.failed === true;
521
+ this.conditionFailureReason = outcome.failed === true && typeof outcome.reason === 'string' ? outcome.reason : '';
451
522
  return
452
523
  }
453
524
  const cond = this.resolvedVisibleWhen;
454
525
  if (!cond) {
455
526
  this.conditionMet = null;
456
527
  this.conditionValue = null;
528
+ this.conditionFailed = false;
529
+ this.conditionFailureReason = '';
457
530
  return
458
531
  }
459
532
  try {
460
533
  const value = await readVisibleWhenValue(cond);
461
534
  this.conditionValue = value;
462
535
  this.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value);
536
+ this.conditionFailed = false;
537
+ this.conditionFailureReason = '';
463
538
  } catch (error) {
464
539
  // Still hidden: a banner that cannot tell whether it applies
465
540
  // should not claim attention. But not silently, because a
@@ -468,6 +543,8 @@ var script = {
468
543
  console.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error);
469
544
  this.conditionMet = false;
470
545
  this.conditionValue = null;
546
+ this.conditionFailed = true;
547
+ this.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '');
471
548
  }
472
549
  },
473
550