@conduction/nextcloud-vue 2.104.0 → 2.106.0

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 (224) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  6. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +18 -0
  7. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  8. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -1
  9. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +18 -0
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  16. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +93 -1
  17. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +6 -3
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -3
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +112 -5
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +9 -0
  28. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js.map +1 -0
  29. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +24 -0
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -0
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +49 -0
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -0
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +36 -18
  35. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +17 -38
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  42. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +1 -1
  43. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  46. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +2 -0
  47. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +32 -29
  49. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  51. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +53 -2
  52. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +119 -111
  54. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  56. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +62 -1
  57. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  60. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +73 -4
  61. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  62. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +74 -27
  63. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +208 -46
  65. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +121 -98
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  69. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +24 -0
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  71. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +20 -4
  72. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  73. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +25 -6
  74. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  75. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +112 -1
  76. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  77. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +100 -44
  78. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  79. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  80. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +67 -4
  81. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  83. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  84. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue.js +9 -0
  85. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue.js.map +1 -0
  86. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue2.js +88 -0
  87. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue2.js.map +1 -0
  88. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue3.js +39 -0
  89. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue3.js.map +1 -0
  90. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  91. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +295 -12
  92. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  93. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +267 -144
  94. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  95. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  96. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +4 -0
  97. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  98. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +3 -1
  99. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  100. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  101. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +93 -1
  102. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  103. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +322 -246
  104. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  105. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue.js +1 -1
  106. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +122 -7
  107. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  108. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +189 -93
  109. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  110. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  111. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  112. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +3 -3
  113. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  114. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  115. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  116. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +16 -4
  117. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  118. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  119. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  120. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  121. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  122. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +42 -0
  123. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  124. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +38 -19
  125. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  126. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js +9 -0
  127. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js.map +1 -0
  128. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js +258 -0
  129. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js.map +1 -0
  130. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js +69 -0
  131. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js.map +1 -0
  132. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  133. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +11 -0
  134. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  135. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -4
  136. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  137. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  138. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  139. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  140. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  141. package/dist/esm/index.js +2 -0
  142. package/dist/esm/index.js.map +1 -1
  143. package/dist/esm/l10n/en.json.js +24 -1
  144. package/dist/esm/l10n/en.json.js.map +1 -1
  145. package/dist/esm/l10n/nl.json.js +24 -1
  146. package/dist/esm/l10n/nl.json.js.map +1 -1
  147. package/dist/esm/library-style.js +1 -1
  148. package/dist/esm/nextcloud-vue.css +1369 -748
  149. package/dist/esm/utils/dueText.js +18 -3
  150. package/dist/esm/utils/dueText.js.map +1 -1
  151. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  152. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  153. package/dist/esm/utils/nextcloudApps.js +45 -0
  154. package/dist/esm/utils/nextcloudApps.js.map +1 -0
  155. package/dist/esm/utils/scopeListLayout.js +9 -2
  156. package/dist/esm/utils/scopeListLayout.js.map +1 -1
  157. package/dist/esm/utils/sortOrder.js +11 -1
  158. package/dist/esm/utils/sortOrder.js.map +1 -1
  159. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  160. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  161. package/dist/esm/utils/visibleWhen.js +12 -4
  162. package/dist/esm/utils/visibleWhen.js.map +1 -1
  163. package/dist/nextcloud-vue.cjs.js +8965 -6497
  164. package/dist/nextcloud-vue.cjs.js.map +2 -2
  165. package/dist/nextcloud-vue.css +1369 -748
  166. package/l10n/en.json +24 -1
  167. package/l10n/nl.json +24 -1
  168. package/package.json +1 -1
  169. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -1
  170. package/src/components/CnCardGrid/CnCardGrid.md +2 -0
  171. package/src/components/CnCardGrid/CnCardGrid.vue +20 -0
  172. package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -1
  173. package/src/components/CnChartWidget/CnChartWidget.md +2 -0
  174. package/src/components/CnChartWidget/CnChartWidget.vue +93 -1
  175. package/src/components/CnDashboardPage/CnDashboardPage.vue +9 -6
  176. package/src/components/CnDetailPage/CnDetailPage.md +3 -2
  177. package/src/components/CnDetailPage/CnDetailPage.vue +112 -5
  178. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +60 -0
  179. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +36 -29
  180. package/src/components/CnFactsWidget/CnFactsWidget.vue +4 -0
  181. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -1
  182. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +4 -1
  183. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +70 -4
  184. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +62 -1
  185. package/src/components/CnFlowsPage/CnFlowBoard.vue +132 -8
  186. package/src/components/CnIndexPage/CnIndexPage.md +8 -5
  187. package/src/components/CnIndexPage/CnIndexPage.vue +236 -54
  188. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +36 -1
  189. package/src/components/CnIndexPage/useSelfFetchList.js +25 -6
  190. package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
  191. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +148 -1
  192. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +68 -5
  193. package/src/components/CnNoticeWidget/CnNoticeWidget.vue +153 -0
  194. package/src/components/CnNoticeWidget/index.js +12 -0
  195. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  196. package/src/components/CnObjectCard/CnObjectCard.vue +354 -13
  197. package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -0
  198. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +190 -4
  199. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +209 -9
  200. package/src/components/CnSettingsField/CnSettingsField.vue +4 -1
  201. package/src/components/CnSetupWizard/CnSetupWizard.vue +9 -1
  202. package/src/components/CnTabs/CnTabs.vue +11 -0
  203. package/src/components/CnTabsWidget/CnTabsWidget.vue +91 -18
  204. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.md +11 -0
  205. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue +382 -0
  206. package/src/components/CnTeamWorkloadWidget/index.js +14 -0
  207. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +15 -4
  208. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  209. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  210. package/src/components/index.js +2 -0
  211. package/src/css/dialog.css +20 -2
  212. package/src/css/index-page.css +14 -2
  213. package/src/css/look-board-detail.css +125 -8
  214. package/src/css/look-board-index.css +100 -4
  215. package/src/css/look-board.css +122 -14
  216. package/src/index.js +2 -1
  217. package/src/schemas/app-manifest-v2.schema.json +110 -8
  218. package/src/utils/dueText.js +18 -3
  219. package/src/utils/libraryWidgetKeys.js +2 -0
  220. package/src/utils/nextcloudApps.js +50 -0
  221. package/src/utils/scopeListLayout.js +9 -2
  222. package/src/utils/sortOrder.js +10 -0
  223. package/src/utils/validateManifestV2.compiled.js +3 -3
  224. package/src/utils/visibleWhen.js +12 -4
@@ -1 +1 @@
1
- {"version":3,"file":"CnFactsWidget.vue3.js","sources":["../../../../src/components/CnFactsWidget/CnFactsWidget.vue?vue&type=template&id=7f141540&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-facts-widget\" data-testid=\"cn-facts-widget\">\n\t\t<p v-if=\"error\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-error\">\n\t\t\t{{ tr('Could not load this information') }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && !record\" class=\"cn-facts-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t</div>\n\t\t<p v-else-if=\"blocks.length === 0\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<template v-for=\"block in blocks\" :key=\"block.key\">\n\t\t\t\t<dl\n\t\t\t\t\tv-if=\"block.kind === 'rows'\"\n\t\t\t\t\tclass=\"cn-facts-widget__rows\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__rows--columns': twoColumns }\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"row in block.rows\"\n\t\t\t\t\t\t:key=\"row.key\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__row\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${row.key}`\">\n\t\t\t\t\t\t<dt class=\"cn-facts-widget__label\" :class=\"{ 'cn-facts-widget__label--icon': row.icon }\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"row.icon\"\n\t\t\t\t\t\t\t\t:name=\"row.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t\t<span :class=\"{ 'hidden-visually': row.icon }\">{{ row.label }}</span>\n\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t<dd class=\"cn-facts-widget__value\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\tv-if=\"row.href\"\n\t\t\t\t\t\t\t\t:href=\"row.href\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__value-link\"\n\t\t\t\t\t\t\t\t:target=\"row.external ? '_blank' : undefined\"\n\t\t\t\t\t\t\t\t:rel=\"row.external ? 'noopener noreferrer' : undefined\">{{ row.value }}</a>\n\t\t\t\t\t\t\t<span v-else>{{ row.masked && !revealed[row.key] ? row.masked : row.value }}</span>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.reveal && !revealed[row.key]\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__inline-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"revealLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-reveal-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"reveal(row)\">\n\t\t\t\t\t\t\t\t<EyeOutline :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t{{ showLabel }}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.copy\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__icon-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:title=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-copy-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"copy(row)\">\n\t\t\t\t\t\t\t\t<Check v-if=\"copied === row.key\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t<ContentCopy v-else :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t<component\n\t\t\t\t\t:is=\"block.to ? 'router-link' : 'strong'\"\n\t\t\t\t\tv-else-if=\"block.kind === 'title'\"\n\t\t\t\t\t:to=\"block.to || undefined\"\n\t\t\t\t\tclass=\"cn-facts-widget__title\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</component>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'line'\"\n\t\t\t\t\tclass=\"cn-facts-widget__line\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'text'\"\n\t\t\t\t\tclass=\"cn-facts-widget__text\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'lead'\"\n\t\t\t\t\tclass=\"cn-facts-widget__lead\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-facts-widget__lead-tile\"\n\t\t\t\t\t\t:class=\"`cn-facts-widget__lead-tile--${block.tone}`\"\n\t\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon v-if=\"block.icon\" :name=\"block.icon\" :size=\"20\" />\n\t\t\t\t\t\t<template v-else>{{ block.initials }}</template>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-facts-widget__lead-text\">\n\t\t\t\t\t\t<strong class=\"cn-facts-widget__lead-title\">{{ block.value }}</strong>\n\t\t\t\t\t\t<span v-if=\"block.subtitle\" class=\"cn-facts-widget__lead-subtitle\">{{ block.subtitle }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'meter'\"\n\t\t\t\t\tclass=\"cn-facts-widget__meter\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__meter--over': block.over }\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<div class=\"cn-facts-widget__meter-head\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-label\">{{ block.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-value\">{{ block.value }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-if=\"block.limit !== null\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__meter-track\"\n\t\t\t\t\t\trole=\"meter\"\n\t\t\t\t\t\t:aria-label=\"block.label\"\n\t\t\t\t\t\t:aria-valuenow=\"block.amount\"\n\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\t:aria-valuemax=\"block.limit\"\n\t\t\t\t\t\t:aria-valuetext=\"block.value\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}-bar`\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-bar\" :style=\"{ width: `${block.percent}%` }\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div v-else-if=\"block.kind === 'pill'\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t:label=\"block.value\"\n\t\t\t\t\t\t:variant=\"block.variant\"\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t\t<NcNoteCard\n\t\t\tv-if=\"notice && record\"\n\t\t\t:type=\"notice.type\"\n\t\t\tclass=\"cn-facts-widget__notice\"\n\t\t\tdata-testid=\"cn-facts-notice\">\n\t\t\t{{ notice.text }}\n\t\t</NcNoteCard>\n\t\t<component\n\t\t\t:is=\"link.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"link && record\"\n\t\t\t:to=\"link.to || undefined\"\n\t\t\t:href=\"link.href || undefined\"\n\t\t\tclass=\"cn-facts-widget__link\"\n\t\t\tdata-testid=\"cn-facts-link\">\n\t\t\t{{ link.label }}\n\t\t</component>\n\t\t<button\n\t\t\tv-if=\"action && record\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-facts-widget__action\"\n\t\t\tdata-testid=\"cn-facts-action\"\n\t\t\t@click=\"runAction(content.action)\">\n\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t{{ action.label }}\n\t\t</button>\n\t</div>\n</template>\n\n<script>\nimport { emit } from '@nextcloud/event-bus'\nimport { formatFileSize } from '@nextcloud/files'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport ContentCopy from 'vue-material-design-icons/ContentCopy.vue'\nimport EyeOutline from 'vue-material-design-icons/EyeOutline.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { resolveEndpointRequest } from '../../composables/useEndpointSource.js'\nimport { useRefLabels } from '../../composables/useRefLabels.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { formatMoney, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\nimport { userDisplayName } from '../../utils/userDisplayName.js'\n\n/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */\nconst KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text', 'meter'])\n\n/**\n * The label resolver every facts widget on the page shares, made on first use\n * (the object store needs an active pinia). One batched request per register and\n * schema for the ids it has not seen; a label it has read stays until the page\n * reloads, and an object the store already holds is not fetched at all.\n *\n * @type {{resolve: Function}|null}\n */\nlet refLabels = null\n\n/**\n * The entry that reads a resolved pill's tone: the same reference, with the\n * referenced object's `toneField` as the field to read.\n *\n * @param {object} spec A facts entry.\n * @return {object|null} `{ path, itemPath, resolve }`, or null when no `toneField` is set.\n */\nfunction toneSpec(spec) {\n\tconst resolve = spec && spec.resolve\n\tif (!resolve || typeof resolve !== 'object' || typeof resolve.toneField !== 'string' || resolve.toneField === '') {\n\t\treturn null\n\t}\n\treturn { path: spec.path, itemPath: spec.itemPath, resolve: { register: resolve.register, schema: resolve.schema, labelField: resolve.toneField } }\n}\n\n/** The page-wide reference label resolver. */\nfunction sharedRefLabels() {\n\tif (!refLabels) {\n\t\trefLabels = useRefLabels()\n\t}\n\treturn refLabels\n}\n\n/**\n * Key of a resolved reference in the widget's `labels` data.\n *\n * @param {{register: string, schema: string, labelField?: string}} resolve The entry's `resolve` config.\n * @param {string} id The referenced object's id.\n * @return {string} The key.\n */\nfunction refKey(resolve, id) {\n\treturn `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`\n}\n\n/** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */\nexport const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'\n\n/**\n * CnFactsWidget — the `facts` widget: a card of facts about ONE object.\n *\n * Renders the configured fields of the current detail-page object, or of one\n * other object named by `content.source`, as the side cards on the screens\n * draw them: label/value rows (a two-column grid, label muted), a bold title\n * line that may link, muted meta lines that join several fields with \" · \",\n * a status pill, and one link at the foot.\n *\n * Everything is configuration; nothing in it knows an app:\n *\n * ```json\n * {\n * \"type\": \"facts\",\n * \"title\": \"Zaak\",\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"id\": \"@object.case\" },\n * \"fields\": [\n * { \"as\": \"title\", \"path\": \"title\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * { \"as\": \"line\", \"parts\": [\"identifier\", \"caseTypeTitle\"] },\n * { \"as\": \"pill\", \"path\": \"status\", \"map\": { \"open\": \"In behandeling\" } },\n * { \"path\": \"deadline\", \"label\": \"Termijn\", \"format\": \"date\" }\n * ],\n * \"link\": { \"label\": \"Naar de zaak\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * \"emptyText\": \"Geen zaak\"\n * }\n * }\n * ```\n *\n * Tokens: `source.id` takes the shared filter tokens; `source.url` takes them\n * inside the url, the endpoint grammar (`/apps/x/api/y/@objectId`)\n * (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@me`, ...). Routes\n * and the link also take `@source.<field>` (a field of the object shown).\n * A source whose id token does not resolve renders the empty text, never a\n * request for the literal token.\n *\n * Labels, map values, the link label and the empty text go through the host\n * app's translate (`cnTranslate`).\n */\nexport default {\n\tname: 'CnFactsWidget',\n\n\tcomponents: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon, NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\tcnObjectContext: { default: null },\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The page's app config; its `currency` is the reporting currency a money value falls back to. */\n\t\tcnAppConfig: { default: null },\n\t\t/** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `fields` — the entries, in order. Each is `{ path, label?, as?, from?, format?, map?, itemPath?, parts?, separator?, route?, variant?, variantMap?, colorMap?, icon?, copy?, reveal?, text?, title?, subtitle?, initialsPath?, tone? }`.\n\t\t * `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`), `pill` or `meter`.\n\t\t * A `meter` shows the amount at `path` against the limit at `limitPath` (or a constant `limit`) as \"12 of 50\" with a bar; past the limit the bar turns red and the text says \"over the limit\"; without a positive limit it shows the amount and no bar.\n\t\t * A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.\n\t\t * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.\n\t\t * `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.\n\t\t * `format: 'user'` shows a Nextcloud user id as the user's display name, read once per id; the entry stays empty until the name is known.\n\t\t * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).\n\t\t * - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar \"Advies ontvangen\"); nothing shows while the value is empty.\n\t\t * - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.\n\t\t * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `list` (an array joined by \", \", each item read at `itemPath`), `bytes` (a size, \"1.5 MB\") or `currency` (a money amount in the reader's language, \"€ 1.234,50\": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).\n\t\t * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default \" · \"); empty parts are dropped.\n\t\t * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: \"object\"` reads the current object even then (a client card that also shows the ticket's channel).\n\t\t * - `link` — `{ label, route? | href? }` at the foot.\n\t\t * - `hideEmpty` — drop an entry whose value is empty (default true).\n\t\t * - `columns` — `2` lays the rows out in two columns with the label above the value (board DqContact); a row with `parts` joins them like a line (an address on one row).\n\t\t * - `emptyText` — shown when no entry has a value.\n\t\t *\n\t\t * @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The current detail-page object, passed by the detail page host.\n\t\t * Read when `content.source` is not set.\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current detail-page object id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header', 'reveal'],\n\n\tdata() {\n\t\treturn {\n\t\t\tfetched: null,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t\t/** Keys of the masked rows the reader chose to show. */\n\t\t\trevealed: {},\n\t\t\t/** Key of the row whose value was just copied. */\n\t\t\tcopied: '',\n\t\t\t/** Labels of the referenced objects read so far, by `refKey`. */\n\t\t\tlabels: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the rows sit in two columns, label above value.\n\t\t *\n\t\t * @spec openspec/changes/facts-columns/specs/facts-widget/spec.md#requirement-facts-rows-can-sit-in-two-columns\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumns() {\n\t\t\treturn Number(this.content?.columns) === 2\n\t\t},\n\n\t\t/**\n\t\t * The token context: the detail object (prop, else the injected context)\n\t\t * and the page workspace.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\ttokenCtx() {\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The source declaration with its tokens resolved, or null for the current object.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedSource() {\n\t\t\tconst source = this.content?.source\n\t\t\tif (!source || typeof source !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof source.url === 'string' && source.url !== '') {\n\t\t\t\t// The endpoint grammar: tokens INSIDE the url, and a request\n\t\t\t\t// that is blocked while one leaves an empty path segment.\n\t\t\t\tconst request = resolveEndpointRequest({ url: source.url }, this.tokenCtx)\n\t\t\t\treturn { url: request.blocked ? '' : request.url }\n\t\t\t}\n\t\t\treturn resolveDeepTokens(source, this.tokenCtx)\n\t\t},\n\n\t\t/**\n\t\t * A stable key of the resolved source, so the widget refetches only when it changes.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''\n\t\t},\n\n\t\t/**\n\t\t * The object the facts are read from.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\trecord() {\n\t\t\treturn this.resolvedSource ? this.fetched : this.objectData\n\t\t},\n\n\t\t/**\n\t\t * The entries to draw, consecutive rows grouped into one list.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tblocks() {\n\t\t\tconst sourceRecord = this.record\n\t\t\tif (!sourceRecord) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst hideEmpty = this.content?.hideEmpty !== false\n\t\t\tconst out = []\n\t\t\tfields.forEach((field, index) => {\n\t\t\t\tif (!field || typeof field !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// `from: \"object\"` reads the page's own object while the card shows a `source`.\n\t\t\t\tconst record = field.from === 'object' && this.objectData ? this.objectData : sourceRecord\n\t\t\t\tconst kind = this.kindOf(field)\n\t\t\t\tconst key = String(field.key || field.path || `${kind}-${index}`).replace(/[^\\w-]/g, '-')\n\t\t\t\tconst value = this.entryValue(record, field, kind)\n\t\t\t\tif (value === '' && (hideEmpty || kind === 'lead')) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (kind === 'row') {\n\t\t\t\t\tconst last = out[out.length - 1]\n\t\t\t\t\tconst row = { key, label: this.tr(field.label || ''), value, ...this.rowExtras(record, field, value) }\n\t\t\t\t\tif (last && last.kind === 'rows') {\n\t\t\t\t\t\tlast.rows.push(row)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tout.push({ kind: 'rows', key: `rows-${index}`, rows: [row] })\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst block = { kind, key, value }\n\t\t\t\tif (kind === 'lead') {\n\t\t\t\t\tblock.subtitle = this.partsValue(record, field.subtitle)\n\t\t\t\t\tblock.icon = typeof field.icon === 'string' ? field.icon : ''\n\t\t\t\t\tblock.initials = block.icon ? '' : this.initialsOf(readPath(record, field.initialsPath))\n\t\t\t\t\tblock.tone = ['success', 'info', 'warning', 'error'].includes(field.tone) ? field.tone : 'neutral'\n\t\t\t\t}\n\t\t\t\tif (kind === 'meter') {\n\t\t\t\t\tObject.assign(block, this.meterOf(record, field))\n\t\t\t\t}\n\t\t\t\tif (kind === 'title' && field.route) {\n\t\t\t\t\tblock.to = this.resolveRoute(field.route, record)\n\t\t\t\t}\n\t\t\t\tif (kind === 'pill') {\n\t\t\t\t\tconst raw = readPath(record, field.path)\n\t\t\t\t\tconst colours = field.variantMap || field.colorMap\n\t\t\t\t\t// A resolved pill looks its tone up by the referenced object's tone field,\n\t\t\t\t\t// then by the resolved label, then by the stored value.\n\t\t\t\t\tconst keys = field.resolve && typeof field.resolve === 'object'\n\t\t\t\t\t\t? [toneSpec(field) ? this.refLabels(raw, toneSpec(field))[0] : undefined, this.refLabels(raw, field)[0], raw]\n\t\t\t\t\t\t: [raw]\n\t\t\t\t\tconst hit = colours ? keys.find((candidate) => (typeof candidate === 'string' || typeof candidate === 'number') && Object.hasOwn(colours, candidate)) : undefined\n\t\t\t\t\tconst variant = hit !== undefined ? colours[hit] : field.variant\n\t\t\t\t\tblock.variant = isBadgeVariant(variant) ? variant : 'default'\n\t\t\t\t}\n\t\t\t\tout.push(block)\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The foot link, resolved, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tlink() {\n\t\t\tconst link = this.content?.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route) {\n\t\t\t\tconst to = this.resolveRoute(link.route, this.record)\n\t\t\t\treturn to ? { label: this.tr(link.label), to } : null\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\tconst href = this.resolveTemplate(link.href, this.record)\n\t\t\t\treturn href ? { label: this.tr(link.label), href } : null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The action under the facts, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\taction() {\n\t\t\treturn this.actionView(this.content?.action)\n\t\t},\n\n\t\t/**\n\t\t * The header action, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\theaderAction() {\n\t\t\treturn this.actionView(this.content?.headerAction)\n\t\t},\n\n\t\t/**\n\t\t * The card header's status pill, read from the record like a `pill`\n\t\t * entry, or null when it has no text.\n\t\t *\n\t\t * @return {{label: string, variant: string}|null}\n\t\t */\n\t\theaderPill() {\n\t\t\tconst pill = this.content?.headerPill\n\t\t\tif (!pill || typeof pill !== 'object' || !this.record) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = typeof pill.path === 'string' && pill.path !== '' ? readPath(this.record, pill.path) : undefined\n\t\t\tconst mapped = pill.map && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(pill.map, raw) ? pill.map[raw] : undefined\n\t\t\tconst text = mapped ?? pill.text ?? raw\n\t\t\tif (typeof text !== 'string' && typeof text !== 'number') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.tr(String(text))\n\t\t\tif (label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst colours = pill.variantMap || pill.colorMap\n\t\t\tconst hit = colours && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(colours, raw) ? colours[raw] : pill.variant\n\t\t\treturn { label, variant: isBadgeVariant(hit) ? hit : 'default' }\n\t\t},\n\n\t\t/**\n\t\t * The note band under the facts: its text translated, its NcNoteCard\n\t\t * type from `variant` (info by default), or null without text.\n\t\t *\n\t\t * @return {{text: string, type: string}|null}\n\t\t */\n\t\tnotice() {\n\t\t\tconst notice = this.content?.notice\n\t\t\tconst text = notice && typeof notice.text === 'string' ? this.tr(notice.text) : ''\n\t\t\tif (text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { text, type: ['success', 'warning', 'error'].includes(notice.variant) ? notice.variant : 'info' }\n\t\t},\n\n\t\t/** @return {string} The reveal button's text. */\n\t\tshowLabel() {\n\t\t\treturn t('nextcloud-vue', 'Show')\n\t\t},\n\n\t\t/**\n\t\t * The empty line.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content?.emptyText || 'Nothing to show yet')\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\t\t},\n\n\t\trecord: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadLabels()\n\t\t\t},\n\t\t},\n\n\t\theaderAction: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\n\t\theaderPill: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The kind of a field entry.\n\t\t *\n\t\t * @param {object} field The entry.\n\t\t * @return {string} `row`, `title`, `line` or `pill`.\n\t\t */\n\t\tkindOf(field) {\n\t\t\tif (KINDS.includes(field.as)) {\n\t\t\t\treturn field.as\n\t\t\t}\n\t\t\treturn field.label || field.icon ? 'row' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * A meter entry's parts: the amount at `path` against the limit at\n\t\t * `limitPath` (or a constant `limit`), both formatted with the entry's\n\t\t * `format`. Without a positive numeric limit there is no bar, only the\n\t\t * amount. Past the limit the text says so, so the state never rests on\n\t\t * the bar's colour alone.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {{label: string, value: string, amount: number|null, limit: number|null, percent: number, over: boolean}}\n\t\t * @spec openspec/changes/facts-meter-row/specs/facts-meter-row/spec.md#requirement-a-facts-entry-can-be-a-meter\n\t\t */\n\t\tmeterOf(record, field) {\n\t\t\tconst amount = Number(readPath(record, field.path))\n\t\t\tconst rawLimit = typeof field.limitPath === 'string' && field.limitPath !== '' ? readPath(record, field.limitPath) : field.limit\n\t\t\tconst limit = rawLimit === null || rawLimit === undefined || rawLimit === '' ? NaN : Number(rawLimit)\n\t\t\tconst label = this.tr(field.label || '')\n\t\t\tconst value = this.valueOf(record, field)\n\t\t\tif (!Number.isFinite(amount) || !Number.isFinite(limit) || limit <= 0) {\n\t\t\t\treturn { label, value, amount: Number.isFinite(amount) ? amount : null, limit: null, percent: 0, over: false }\n\t\t\t}\n\t\t\tconst over = amount > limit\n\t\t\tconst limitText = this.format(limit, field, record)\n\t\t\treturn {\n\t\t\t\tlabel,\n\t\t\t\tvalue: over\n\t\t\t\t\t? t('nextcloud-vue', '{value} of {limit}, over the limit', { value, limit: limitText })\n\t\t\t\t\t: t('nextcloud-vue', '{value} of {limit}', { value, limit: limitText }),\n\n\t\t\t\tamount,\n\t\t\t\tlimit,\n\t\t\t\tpercent: Math.max(0, Math.min(100, (amount / limit) * 100)),\n\t\t\t\tover,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The display text of an entry of any kind: a lead's title, the parts of\n\t\t * a line or text, a constant `text`, else the value at `path`.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} kind The entry's kind.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\tentryValue(record, field, kind) {\n\t\t\tif (kind === 'lead') {\n\t\t\t\treturn this.partsValue(record, field.title)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text' || kind === 'row') && Array.isArray(field.parts)) {\n\t\t\t\treturn this.lineValue(record, field)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {\n\t\t\t\treturn this.tr(field.text)\n\t\t\t}\n\t\t\treturn this.valueOf(record, field)\n\t\t},\n\n\t\t/**\n\t\t * A lead's title or subtitle: a path, a part spec, or an array of parts\n\t\t * joined as a line joins them.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {string|object|Array} spec The declaration.\n\t\t * @return {string}\n\t\t */\n\t\tpartsValue(record, spec) {\n\t\t\tif (typeof spec === 'string' && spec !== '') {\n\t\t\t\treturn this.valueOf(record, { path: spec })\n\t\t\t}\n\t\t\tif (Array.isArray(spec)) {\n\t\t\t\treturn this.lineValue(record, { parts: spec })\n\t\t\t}\n\t\t\tif (spec && typeof spec === 'object') {\n\t\t\t\treturn this.lineValue(record, { parts: [spec] })\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The initials of a name: \"Sanne de Vries\" reads \"SV\".\n\t\t *\n\t\t * @param {unknown} name The name.\n\t\t * @return {string}\n\t\t */\n\t\tinitialsOf(name) {\n\t\t\tif (typeof name !== 'string' || name.trim() === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst words = name.trim().split(/\\s+/).filter((word) => /^\\p{Lu}/u.test(word))\n\t\t\tconst picked = words.length ? words : name.trim().split(/\\s+/)\n\t\t\tconst first = picked[0].charAt(0)\n\t\t\tconst last = picked.length > 1 ? picked[picked.length - 1].charAt(0) : ''\n\t\t\treturn (first + last).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * What a row adds to its value: an icon, a link for the email, phone\n\t\t * and url formats, the masked text, and the copy and reveal buttons.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} value The display text.\n\t\t * @return {object}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trowExtras(record, field, value) {\n\t\t\tconst extras = {\n\t\t\t\ticon: typeof field.icon === 'string' ? field.icon : '',\n\t\t\t\tpath: field.path || '',\n\t\t\t\traw: readPath(record, field.path),\n\t\t\t\tcopy: field.copy === true && value !== '',\n\t\t\t\treveal: false,\n\t\t\t\tmasked: '',\n\t\t\t\thref: '',\n\t\t\t\texternal: false,\n\t\t\t}\n\t\t\tif (value === '') {\n\t\t\t\treturn extras\n\t\t\t}\n\t\t\tif (field.format === 'email') {\n\t\t\t\textras.href = `mailto:${value}`\n\t\t\t} else if (field.format === 'phone') {\n\t\t\t\textras.href = `tel:${value.replace(/[^\\d+]/g, '')}`\n\t\t\t} else if (field.format === 'url') {\n\t\t\t\textras.href = /^https?:\\/\\//i.test(value) ? value : `https://${value}`\n\t\t\t\textras.external = true\n\t\t\t} else if (field.format === 'masked') {\n\t\t\t\textras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value\n\t\t\t\textras.reveal = field.reveal === true && value.length > 4\n\t\t\t}\n\t\t\treturn extras\n\t\t},\n\n\t\t/**\n\t\t * Show a masked value, and say so: the `reveal` event and the event-bus\n\t\t * channel {@link FACTS_REVEAL_CHANNEL}, so an app can log who saw it.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\treveal(row) {\n\t\t\tthis.revealed = { ...this.revealed, [row.key]: true }\n\t\t\tconst detail = { path: row.path }\n\t\t\t/**\n\t\t\t * @event reveal Emitted when the reader shows a masked value.\n\t\t\t * @type {{path: string}} The path of the shown field.\n\t\t\t */\n\t\t\tthis.$emit('reveal', detail)\n\t\t\temit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null })\n\t\t},\n\n\t\t/**\n\t\t * Copy a row's value to the clipboard.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync copy(row) {\n\t\t\tconst text = row.raw === null || row.raw === undefined || typeof row.raw === 'object' ? row.value : String(row.raw)\n\t\t\ttry {\n\t\t\t\tawait navigator.clipboard.writeText(text)\n\t\t\t\tthis.copied = row.key\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (this.copied === row.key) {\n\t\t\t\t\t\tthis.copied = ''\n\t\t\t\t\t}\n\t\t\t\t}, 2000)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] could not copy:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The copy button's accessible name.\n\t\t */\n\t\tcopyLabel(row) {\n\t\t\treturn this.copied === row.key\n\t\t\t\t? t('nextcloud-vue', 'Copied')\n\t\t\t\t: t('nextcloud-vue', 'Copy {label}', { label: row.label || row.value })\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The reveal button's accessible name.\n\t\t */\n\t\trevealLabel(row) {\n\t\t\treturn t('nextcloud-vue', 'Show {label}', { label: row.label || '' }).trim()\n\t\t},\n\n\t\t/**\n\t\t * The drawn part of an action declaration.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {{label: string, icon: string}|null}\n\t\t */\n\t\tactionView(action) {\n\t\t\tif (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { label: this.tr(action.label), icon: typeof action.icon === 'string' ? action.icon : '' }\n\t\t},\n\n\t\t/**\n\t\t * Dispatch an action with the record's tokens resolved (`@objectId`,\n\t\t * `@object.<field>`), through the page's dispatcher when there is one.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {unknown}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trunAction(action) {\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(resolved)\n\t\t\t}\n\t\t\treturn dispatchAction(resolved, { router: this.$router || null })\n\t\t},\n\n\t\t/** Tell the card around the facts what its header shows (the header action). */\n\t\temitHeader() {\n\t\t\tconst view = this.headerAction\n\t\t\t/**\n\t\t\t * @event header Emitted with the card header's parts: the header action, if any.\n\t\t\t * @type {{link: null, note: string, action: (object|null), pill: (object|null)}} `action` is `{ label, icon, run }`, `pill` is `{ label, variant }`.\n\t\t\t */\n\t\t\tthis.$emit('header', {\n\t\t\t\tlink: null,\n\t\t\t\tnote: '',\n\t\t\t\taction: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,\n\t\t\t\tpill: this.headerPill,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The text of a reference: the labels of the objects its id(s) name, joined\n\t\t * by \", \". An id whose label is not known yet (or cannot be read) adds nothing.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids (or of `{ id }` objects).\n\t\t * @param {object} spec `{ resolve: { register, schema, labelField? }, itemPath? }`.\n\t\t * @return {string}\n\t\t */\n\t\trefText(raw, spec) {\n\t\t\treturn this.refLabels(raw, spec)\n\t\t\t\t.map((label) => (spec.map && Object.hasOwn(spec.map, label) ? this.tr(spec.map[label]) : label))\n\t\t\t\t.join(', ')\n\t\t},\n\n\t\t/**\n\t\t * The labels of a reference as read, before `map` is applied: the key a\n\t\t * pill's `variantMap` and `colorMap` look up.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids.\n\t\t * @param {object} spec `{ resolve, itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefLabels(raw, spec) {\n\t\t\treturn this.refIds(raw, spec)\n\t\t\t\t.map((id) => this.labels[refKey(spec.resolve, id)])\n\t\t\t\t.filter((label) => typeof label === 'string' && label !== '')\n\t\t},\n\n\t\t/**\n\t\t * The ids a reference value holds.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefIds(raw, spec) {\n\t\t\treturn (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t.map((item) => (item && typeof item === 'object' ? (spec.itemPath ? readPath(item, spec.itemPath) : (item.id ?? item.uuid)) : item))\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')\n\t\t\t\t.map(String)\n\t\t},\n\n\t\t/**\n\t\t * Every entry or part that carries `resolve`, with the record's value at its path.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @return {Array<{spec: object, raw: unknown}>}\n\t\t */\n\t\trefSpecs(record) {\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst specs = []\n\t\t\tconst visit = (spec) => {\n\t\t\t\tif (!spec || typeof spec !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (spec.resolve && typeof spec.resolve === 'object' && spec.path) {\n\t\t\t\t\tspecs.push({ spec, raw: readPath(record, spec.path) })\n\t\t\t\t\tif (toneSpec(spec)) {\n\t\t\t\t\t\tspecs.push({ spec: toneSpec(spec), raw: readPath(record, spec.path) })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst nested = [...(Array.isArray(spec.parts) ? spec.parts : []), ...(Array.isArray(spec.title) ? spec.title : [spec.title]), ...(Array.isArray(spec.subtitle) ? spec.subtitle : [spec.subtitle])]\n\t\t\t\tnested.forEach(visit)\n\t\t\t}\n\t\t\tfields.forEach(visit)\n\t\t\treturn specs\n\t\t},\n\n\t\t/**\n\t\t * Read the labels of the referenced objects, one batched request per\n\t\t * register and schema, and keep them in `labels`.\n\t\t *\n\t\t * @param {object|null} record The object shown (defaults to the current record).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLabels(record = this.record) {\n\t\t\tif (!record) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst groups = new Map()\n\t\t\tfor (const { spec, raw } of this.refSpecs(record)) {\n\t\t\t\tconst { register, schema, labelField } = spec.resolve\n\t\t\t\tif (!register || !schema) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst groupKey = `${register}/${schema}/${labelField || ''}`\n\t\t\t\tconst group = groups.get(groupKey) || { resolve: spec.resolve, ids: new Set() }\n\t\t\t\tthis.refIds(raw, spec).forEach((id) => group.ids.add(id))\n\t\t\t\tgroups.set(groupKey, group)\n\t\t\t}\n\t\t\tif (groups.size === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolver = sharedRefLabels()\n\t\t\tconst next = { ...this.labels }\n\t\t\tawait Promise.all([...groups.values()].map(async ({ resolve, ids }) => {\n\t\t\t\tconst found = await resolver.resolve(resolve.register, resolve.schema, [...ids], resolve.labelField)\n\t\t\t\tObject.entries(found).forEach(([id, label]) => {\n\t\t\t\t\tnext[refKey(resolve, id)] = label\n\t\t\t\t})\n\t\t\t}))\n\t\t\tthis.labels = next\n\t\t},\n\n\t\t/**\n\t\t * Read the source object.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst source = this.resolvedSource\n\t\t\tthis.error = false\n\t\t\tif (!source) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst url = this.sourceUrl(source)\n\t\t\tif (!url) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url\n\t\t\t\tconst response = await axios.get(full)\n\t\t\t\tconst data = response && response.data\n\t\t\t\tthis.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] the source could not be read:', e?.message || e)\n\t\t\t\tthis.fetched = null\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The URL of a resolved source, or '' when it cannot be read yet.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @return {string}\n\t\t */\n\t\tsourceUrl(source) {\n\t\t\tif (typeof source.url === 'string') {\n\t\t\t\treturn source.url\n\t\t\t}\n\t\t\tif (hasUnresolvedDeepTokens(source)) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst { register, schema, id } = source\n\t\t\tif (!register || !schema || id === undefined || id === null || id === '' || typeof id === 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\n\t\t/**\n\t\t * The display text of one field.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tvalueOf(record, field) {\n\t\t\tconst raw = readPath(record, field.path)\n\t\t\treturn this.format(raw, field, record)\n\t\t},\n\n\t\t/**\n\t\t * The text of a `line` entry that joins several parts.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tlineValue(record, field) {\n\t\t\tconst separator = typeof field.separator === 'string' ? field.separator : ' · '\n\t\t\treturn field.parts\n\t\t\t\t.map((part) => {\n\t\t\t\t\tconst spec = typeof part === 'string' ? { path: part } : (part || {})\n\t\t\t\t\t// A constant part (`{ text }`): \"Handelsregister\" after the legal form.\n\t\t\t\t\tif (!spec.path && typeof spec.text === 'string') {\n\t\t\t\t\t\treturn this.tr(spec.text)\n\t\t\t\t\t}\n\t\t\t\t\t// A group of parts with its own separator (\"Parkstraat 24\" inside\n\t\t\t\t\t// \"Parkstraat 24, 3421 AB Zuiddrecht\").\n\t\t\t\t\tif (!spec.path && Array.isArray(spec.parts)) {\n\t\t\t\t\t\treturn this.lineValue(record, spec)\n\t\t\t\t\t}\n\t\t\t\t\tconst value = this.format(readPath(record, spec.path), spec, record)\n\t\t\t\t\tif (value === '') {\n\t\t\t\t\t\treturn ''\n\t\t\t\t\t}\n\t\t\t\t\treturn spec.label ? `${this.tr(spec.label)} ${value}` : value\n\t\t\t\t})\n\t\t\t\t.filter((value) => value !== '')\n\t\t\t\t.join(separator)\n\t\t},\n\n\t\t/**\n\t\t * Format a raw value.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ format?, map?, itemPath?, currency?, currencyField?, decimals? }`.\n\t\t * @param {object|null} [record] The object shown, which `currencyField` reads.\n\t\t * @return {string} The text, '' when empty.\n\t\t * @spec openspec/changes/facts-currency-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-money-amount\n\t\t * @spec openspec/changes/facts-user-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-user-by-name\n\t\t */\n\t\tformat(raw, spec, record = null) {\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (spec.resolve && typeof spec.resolve === 'object') {\n\t\t\t\treturn this.refText(raw, spec)\n\t\t\t}\n\t\t\tconst format = spec.format || 'text'\n\t\t\tif (format === 'list' || Array.isArray(raw)) {\n\t\t\t\tconst items = (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t\t.map((item) => (spec.itemPath && item && typeof item === 'object' ? readPath(item, spec.itemPath) : item))\n\t\t\t\t\t.map((item) => this.format(item, { ...spec, format: 'text' }))\n\t\t\t\t\t.filter((item) => item !== '')\n\t\t\t\treturn items.join(', ')\n\t\t\t}\n\t\t\tif (format === 'date' || format === 'date-long') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\tif (date === null) {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\t// A year far from now forces the long form, so date-long reuses the board formatter.\n\t\t\t\treturn format === 'date-long'\n\t\t\t\t\t? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))\n\t\t\t\t\t: formatBoardDate(date)\n\t\t\t}\n\t\t\tif (format === 'bytes') {\n\t\t\t\tconst bytes = Number(raw)\n\t\t\t\treturn Number.isFinite(bytes) && bytes >= 0 ? formatFileSize(bytes) : ''\n\t\t\t}\n\t\t\tif (format === 'user') {\n\t\t\t\treturn userDisplayName(raw)\n\t\t\t}\n\t\t\tif (format === 'currency') {\n\t\t\t\tconst money = formatMoney(raw, { currency: spec.currency, currencyField: spec.currencyField, decimals: spec.decimals }, record, unwrapAppConfig(this.cnAppConfig))\n\t\t\t\tif (money !== '') {\n\t\t\t\t\treturn money\n\t\t\t\t}\n\t\t\t\t// Not a number: shown as written, never as \"[object Object]\".\n\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t}\n\t\t\tif (typeof raw === 'object') {\n\t\t\t\tconst label = raw.title || raw.name || raw.label || raw.displayName\n\t\t\t\treturn typeof label === 'string' ? label : ''\n\t\t\t}\n\t\t\tif (typeof raw === 'boolean') {\n\t\t\t\tconst mapped = spec.map && spec.map[String(raw)]\n\t\t\t\treturn mapped ? this.tr(mapped) : this.tr(raw ? 'Yes' : 'No')\n\t\t\t}\n\t\t\tif (spec.map && Object.hasOwn(spec.map, String(raw))) {\n\t\t\t\treturn this.tr(spec.map[String(raw)])\n\t\t\t}\n\t\t\treturn String(raw)\n\t\t},\n\n\t\t/**\n\t\t * Replace `@source.<field>` / `@source.id` and the shared tokens in a string.\n\t\t *\n\t\t * @param {string} value The template.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {string} The value, '' when a token stays unresolved.\n\t\t */\n\t\tresolveTemplate(value, record) {\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tlet unresolved = false\n\t\t\tconst replaced = value.replace(/@source\\.([\\w.@]+)/g, (match, path) => {\n\t\t\t\tconst read = record ? (path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)) : undefined\n\t\t\t\tif (read === undefined || read === null || read === '') {\n\t\t\t\t\tunresolved = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(read)\n\t\t\t})\n\t\t\tif (unresolved) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (replaced.charAt(0) === '@') {\n\t\t\t\tconst resolved = resolveDeepTokens(replaced, this.tokenCtx)\n\t\t\t\treturn typeof resolved === 'string' && resolved.charAt(0) === '@' ? '' : String(resolved)\n\t\t\t}\n\t\t\treturn replaced\n\t\t},\n\n\t\t/**\n\t\t * Resolve a route declaration (`{ name, params?, query? }`).\n\t\t *\n\t\t * @param {object} route The declaration.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {object|null} The route, or null when a param stays unresolved.\n\t\t */\n\t\tresolveRoute(route, record) {\n\t\t\tif (!route || typeof route !== 'object' || !route.name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst out = { name: route.name }\n\t\t\tfor (const key of ['params', 'query']) {\n\t\t\t\tif (route[key] && typeof route[key] === 'object') {\n\t\t\t\t\tout[key] = {}\n\t\t\t\t\tfor (const [name, value] of Object.entries(route[key])) {\n\t\t\t\t\t\tconst resolved = this.resolveTemplate(value, record)\n\t\t\t\t\t\tif (resolved === '') {\n\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t}\n\t\t\t\t\t\tout[key][name] = resolved\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-facts-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__rows {\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\tgap: 8px 16px;\n\tmargin: 0;\n\tfont-size: 14px;\n}\n\n/* Each row is a grid of its own children: the wrapper takes no box. */\n.cn-facts-widget__row {\n\tdisplay: contents;\n}\n\n/* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which\n pushed the label to the right of its column and added 12px around both. The\n class is doubled so these resets outrank that rule, and a dossiq-style\n `:deep()` override in an app is not needed. */\n.cn-facts-widget__label.cn-facts-widget__label {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__value.cn-facts-widget__value {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tfont-weight: 500;\n\tmin-width: 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-facts-widget__title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__line {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__meter {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tfont-size: 14px;\n}\n\n.cn-facts-widget__meter-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 16px;\n}\n\n.cn-facts-widget__meter-label {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__meter-value {\n\tfont-weight: 500;\n\ttext-align: end;\n}\n\n.cn-facts-widget__meter-track {\n\theight: 6px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-darker);\n\toverflow: hidden;\n}\n\n.cn-facts-widget__meter-bar {\n\tdisplay: block;\n\theight: 100%;\n\tborder-radius: inherit;\n\tbackground: var(--color-primary-element);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-value {\n\tcolor: var(--color-error-text);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-bar {\n\tbackground: var(--color-error);\n}\n\n.cn-facts-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__label .hidden-visually {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n\n.cn-facts-widget__label--icon {\n\talign-items: center;\n\tdisplay: flex;\n}\n\n.cn-facts-widget__value-link {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-facts-widget__inline-button,\n.cn-facts-widget__icon-button,\n.cn-facts-widget__action {\n\talign-items: center;\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n}\n\n.cn-facts-widget__inline-button {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: 16px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tgap: 4px;\n\tmargin-inline-start: 8px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n}\n\n.cn-facts-widget__icon-button {\n\tbackground: transparent;\n\tborder: 0;\n\tborder-radius: 6px;\n\tcolor: var(--color-text-maxcontrast);\n\tmargin-inline-start: 4px;\n\tpadding: 2px;\n\tvertical-align: middle;\n}\n\n.cn-facts-widget__icon-button:hover,\n.cn-facts-widget__icon-button:focus-visible {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__text {\n\tcolor: var(--color-main-text);\n\tfont-size: 15px;\n}\n\n/* A lead: an icon tile or initials beside a bold title and a muted subtitle. */\n.cn-facts-widget__lead {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-facts-widget__lead-tile {\n\talign-items: center;\n\tbackground: var(--color-background-dark);\n\tborder-radius: 8px;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 14px;\n\tfont-weight: 700;\n\theight: 40px;\n\tjustify-content: center;\n\twidth: 40px;\n}\n\n.cn-facts-widget__lead-tile--success {\n\tbackground: color-mix(in srgb, var(--color-success) 14%, var(--color-main-background));\n\tborder-radius: 50%;\n\tcolor: var(--color-success-text, var(--color-success));\n}\n\n.cn-facts-widget__lead-tile--info {\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n}\n\n.cn-facts-widget__lead-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__lead-title {\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__lead-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-facts-widget__action {\n\talign-self: flex-start;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n}\n\n.cn-facts-widget__action:hover,\n.cn-facts-widget__action:focus-visible {\n\tbackground: var(--color-primary-element-light-hover, var(--color-primary-element-light));\n}\n\n.cn-facts-widget__notice {\n\tmargin: 4px 0 0;\n}\n\n.cn-facts-widget__link {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n/* `columns: 2` (board DqContact): two columns, the label above the value, a\n hairline under each fact; one column on a phone. */\n.cn-facts-widget__rows--columns {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\tgap: 14px 24px;\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__rows--columns .cn-facts-widget__row {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 3px;\n\tmin-width: 0;\n\tpadding-bottom: 10px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-facts-widget__rows--columns .cn-facts-widget__label.cn-facts-widget__label {\n\tfont-size: 13px;\n}\n\n@media (max-width: 640px) {\n\t.cn-facts-widget__rows--columns {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createVNode","_Fragment","_renderList","_normalizeClass","_createElementVNode","_createBlock","_createTextVNode","_resolveDynamicComponent","_normalizeStyle"],"mappings":"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"}
1
+ {"version":3,"file":"CnFactsWidget.vue3.js","sources":["../../../../src/components/CnFactsWidget/CnFactsWidget.vue?vue&type=template&id=157a16f8&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-facts-widget\" data-testid=\"cn-facts-widget\">\n\t\t<p v-if=\"error\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-error\">\n\t\t\t{{ tr('Could not load this information') }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && !record\" class=\"cn-facts-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t</div>\n\t\t<p v-else-if=\"blocks.length === 0\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<template v-for=\"block in blocks\" :key=\"block.key\">\n\t\t\t\t<dl\n\t\t\t\t\tv-if=\"block.kind === 'rows'\"\n\t\t\t\t\tclass=\"cn-facts-widget__rows\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__rows--columns': twoColumns }\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"row in block.rows\"\n\t\t\t\t\t\t:key=\"row.key\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__row\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${row.key}`\">\n\t\t\t\t\t\t<dt class=\"cn-facts-widget__label\" :class=\"{ 'cn-facts-widget__label--icon': row.icon }\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"row.icon\"\n\t\t\t\t\t\t\t\t:name=\"row.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t\t<span :class=\"{ 'hidden-visually': row.icon }\">{{ row.label }}</span>\n\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t<dd class=\"cn-facts-widget__value\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\tv-if=\"row.href\"\n\t\t\t\t\t\t\t\t:href=\"row.href\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__value-link\"\n\t\t\t\t\t\t\t\t:target=\"row.external ? '_blank' : undefined\"\n\t\t\t\t\t\t\t\t:rel=\"row.external ? 'noopener noreferrer' : undefined\">{{ row.value }}</a>\n\t\t\t\t\t\t\t<span v-else>{{ row.masked && !revealed[row.key] ? row.masked : row.value }}</span>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.reveal && !revealed[row.key]\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__inline-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"revealLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-reveal-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"reveal(row)\">\n\t\t\t\t\t\t\t\t<EyeOutline :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t{{ showLabel }}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.copy\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__icon-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:title=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-copy-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"copy(row)\">\n\t\t\t\t\t\t\t\t<Check v-if=\"copied === row.key\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t<ContentCopy v-else :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t<component\n\t\t\t\t\t:is=\"block.to ? 'router-link' : 'strong'\"\n\t\t\t\t\tv-else-if=\"block.kind === 'title'\"\n\t\t\t\t\t:to=\"block.to || undefined\"\n\t\t\t\t\tclass=\"cn-facts-widget__title\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</component>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'line'\"\n\t\t\t\t\tclass=\"cn-facts-widget__line\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'text'\"\n\t\t\t\t\tclass=\"cn-facts-widget__text\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'lead'\"\n\t\t\t\t\tclass=\"cn-facts-widget__lead\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-facts-widget__lead-tile\"\n\t\t\t\t\t\t:class=\"`cn-facts-widget__lead-tile--${block.tone}`\"\n\t\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon v-if=\"block.icon\" :name=\"block.icon\" :size=\"20\" />\n\t\t\t\t\t\t<template v-else>{{ block.initials }}</template>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-facts-widget__lead-text\">\n\t\t\t\t\t\t<strong class=\"cn-facts-widget__lead-title\">{{ block.value }}</strong>\n\t\t\t\t\t\t<span v-if=\"block.subtitle\" class=\"cn-facts-widget__lead-subtitle\">{{ block.subtitle }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'meter'\"\n\t\t\t\t\tclass=\"cn-facts-widget__meter\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__meter--over': block.over }\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<div class=\"cn-facts-widget__meter-head\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-label\">{{ block.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-value\">{{ block.value }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-if=\"block.limit !== null\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__meter-track\"\n\t\t\t\t\t\trole=\"meter\"\n\t\t\t\t\t\t:aria-label=\"block.label\"\n\t\t\t\t\t\t:aria-valuenow=\"block.amount\"\n\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\t:aria-valuemax=\"block.limit\"\n\t\t\t\t\t\t:aria-valuetext=\"block.value\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}-bar`\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-bar\" :style=\"{ width: `${block.percent}%` }\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div v-else-if=\"block.kind === 'pill'\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t:label=\"block.value\"\n\t\t\t\t\t\t:variant=\"block.variant\"\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t\t<NcNoteCard\n\t\t\tv-if=\"notice && record\"\n\t\t\t:type=\"notice.type\"\n\t\t\tclass=\"cn-facts-widget__notice\"\n\t\t\tdata-testid=\"cn-facts-notice\">\n\t\t\t{{ notice.text }}\n\t\t</NcNoteCard>\n\t\t<component\n\t\t\t:is=\"link.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"link && record\"\n\t\t\t:to=\"link.to || undefined\"\n\t\t\t:href=\"link.href || undefined\"\n\t\t\tclass=\"cn-facts-widget__link\"\n\t\t\tdata-testid=\"cn-facts-link\">\n\t\t\t{{ link.label }}\n\t\t</component>\n\t\t<button\n\t\t\tv-if=\"action && record\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-facts-widget__action\"\n\t\t\tdata-testid=\"cn-facts-action\"\n\t\t\t@click=\"runAction(content.action)\">\n\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t{{ action.label }}\n\t\t</button>\n\t</div>\n</template>\n\n<script>\nimport { emit } from '@nextcloud/event-bus'\nimport { formatFileSize } from '@nextcloud/files'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport ContentCopy from 'vue-material-design-icons/ContentCopy.vue'\nimport EyeOutline from 'vue-material-design-icons/EyeOutline.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { resolveEndpointRequest } from '../../composables/useEndpointSource.js'\nimport { useRefLabels } from '../../composables/useRefLabels.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { formatMoney, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\nimport { userDisplayName } from '../../utils/userDisplayName.js'\n\n/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */\nconst KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text', 'meter'])\n\n/**\n * The label resolver every facts widget on the page shares, made on first use\n * (the object store needs an active pinia). One batched request per register and\n * schema for the ids it has not seen; a label it has read stays until the page\n * reloads, and an object the store already holds is not fetched at all.\n *\n * @type {{resolve: Function}|null}\n */\nlet refLabels = null\n\n/**\n * The entry that reads a resolved pill's tone: the same reference, with the\n * referenced object's `toneField` as the field to read.\n *\n * @param {object} spec A facts entry.\n * @return {object|null} `{ path, itemPath, resolve }`, or null when no `toneField` is set.\n */\nfunction toneSpec(spec) {\n\tconst resolve = spec && spec.resolve\n\tif (!resolve || typeof resolve !== 'object' || typeof resolve.toneField !== 'string' || resolve.toneField === '') {\n\t\treturn null\n\t}\n\treturn { path: spec.path, itemPath: spec.itemPath, resolve: { register: resolve.register, schema: resolve.schema, labelField: resolve.toneField } }\n}\n\n/** The page-wide reference label resolver. */\nfunction sharedRefLabels() {\n\tif (!refLabels) {\n\t\trefLabels = useRefLabels()\n\t}\n\treturn refLabels\n}\n\n/**\n * Key of a resolved reference in the widget's `labels` data.\n *\n * @param {{register: string, schema: string, labelField?: string}} resolve The entry's `resolve` config.\n * @param {string} id The referenced object's id.\n * @return {string} The key.\n */\nfunction refKey(resolve, id) {\n\treturn `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`\n}\n\n/** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */\nexport const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'\n\n/**\n * CnFactsWidget — the `facts` widget: a card of facts about ONE object.\n *\n * Renders the configured fields of the current detail-page object, or of one\n * other object named by `content.source`, as the side cards on the screens\n * draw them: label/value rows (a two-column grid, label muted), a bold title\n * line that may link, muted meta lines that join several fields with \" · \",\n * a status pill, and one link at the foot.\n *\n * Everything is configuration; nothing in it knows an app:\n *\n * ```json\n * {\n * \"type\": \"facts\",\n * \"title\": \"Zaak\",\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"id\": \"@object.case\" },\n * \"fields\": [\n * { \"as\": \"title\", \"path\": \"title\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * { \"as\": \"line\", \"parts\": [\"identifier\", \"caseTypeTitle\"] },\n * { \"as\": \"pill\", \"path\": \"status\", \"map\": { \"open\": \"In behandeling\" } },\n * { \"path\": \"deadline\", \"label\": \"Termijn\", \"format\": \"date\" }\n * ],\n * \"link\": { \"label\": \"Naar de zaak\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * \"emptyText\": \"Geen zaak\"\n * }\n * }\n * ```\n *\n * Tokens: `source.id` takes the shared filter tokens; `source.url` takes them\n * inside the url, the endpoint grammar (`/apps/x/api/y/@objectId`)\n * (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@me`, ...). Routes\n * and the link also take `@source.<field>` (a field of the object shown).\n * A source whose id token does not resolve renders the empty text, never a\n * request for the literal token.\n *\n * Labels, map values, the link label and the empty text go through the host\n * app's translate (`cnTranslate`).\n */\nexport default {\n\tname: 'CnFactsWidget',\n\n\tcomponents: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon, NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\tcnObjectContext: { default: null },\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The page's app config; its `currency` is the reporting currency a money value falls back to. */\n\t\tcnAppConfig: { default: null },\n\t\t/** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `fields` — the entries, in order. Each is `{ path, label?, as?, from?, format?, map?, itemPath?, parts?, separator?, route?, variant?, variantMap?, colorMap?, icon?, copy?, reveal?, text?, title?, subtitle?, initialsPath?, tone? }`.\n\t\t * `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`), `pill` or `meter`.\n\t\t * A `meter` shows the amount at `path` against the limit at `limitPath` (or a constant `limit`) as \"12 of 50\" with a bar; past the limit the bar turns red and the text says \"over the limit\"; without a positive limit it shows the amount and no bar.\n\t\t * A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.\n\t\t * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.\n\t\t * `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.\n\t\t * `format: 'user'` shows a Nextcloud user id as the user's display name, read once per id; the entry stays empty until the name is known.\n\t\t * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).\n\t\t * - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar \"Advies ontvangen\"); nothing shows while the value is empty.\n\t\t * - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.\n\t\t * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `list` (an array joined by \", \", each item read at `itemPath`), `bytes` (a size, \"1.5 MB\") or `currency` (a money amount in the reader's language, \"€ 1.234,50\": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).\n\t\t * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default \" · \"); empty parts are dropped.\n\t\t * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: \"object\"` reads the current object even then (a client card that also shows the ticket's channel).\n\t\t * - `link` — `{ label, route? | href? }` at the foot.\n\t\t * - `hideEmpty` — drop an entry whose value is empty (default true).\n\t\t * - `columns` — `2` lays the rows out in two columns with the label above the value (board DqContact); a row with `parts` joins them like a line (an address on one row).\n\t\t * - `emptyText` — shown when no entry has a value.\n\t\t *\n\t\t * @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The current detail-page object, passed by the detail page host.\n\t\t * Read when `content.source` is not set.\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current detail-page object id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header', 'reveal'],\n\n\tdata() {\n\t\treturn {\n\t\t\tfetched: null,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t\t/** Keys of the masked rows the reader chose to show. */\n\t\t\trevealed: {},\n\t\t\t/** Key of the row whose value was just copied. */\n\t\t\tcopied: '',\n\t\t\t/** Labels of the referenced objects read so far, by `refKey`. */\n\t\t\tlabels: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the rows sit in two columns, label above value.\n\t\t *\n\t\t * @spec openspec/changes/facts-columns/specs/facts-widget/spec.md#requirement-facts-rows-can-sit-in-two-columns\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumns() {\n\t\t\treturn Number(this.content?.columns) === 2\n\t\t},\n\n\t\t/**\n\t\t * The token context: the detail object (prop, else the injected context)\n\t\t * and the page workspace.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\ttokenCtx() {\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The source declaration with its tokens resolved, or null for the current object.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedSource() {\n\t\t\tconst source = this.content?.source\n\t\t\tif (!source || typeof source !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof source.url === 'string' && source.url !== '') {\n\t\t\t\t// The endpoint grammar: tokens INSIDE the url, and a request\n\t\t\t\t// that is blocked while one leaves an empty path segment.\n\t\t\t\tconst request = resolveEndpointRequest({ url: source.url }, this.tokenCtx)\n\t\t\t\treturn { url: request.blocked ? '' : request.url }\n\t\t\t}\n\t\t\treturn resolveDeepTokens(source, this.tokenCtx)\n\t\t},\n\n\t\t/**\n\t\t * A stable key of the resolved source, so the widget refetches only when it changes.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''\n\t\t},\n\n\t\t/**\n\t\t * The object the facts are read from.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\trecord() {\n\t\t\treturn this.resolvedSource ? this.fetched : this.objectData\n\t\t},\n\n\t\t/**\n\t\t * The entries to draw, consecutive rows grouped into one list.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tblocks() {\n\t\t\tconst sourceRecord = this.record\n\t\t\tif (!sourceRecord) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst hideEmpty = this.content?.hideEmpty !== false\n\t\t\tconst out = []\n\t\t\tfields.forEach((field, index) => {\n\t\t\t\tif (!field || typeof field !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// `from: \"object\"` reads the page's own object while the card shows a `source`.\n\t\t\t\tconst record = field.from === 'object' && this.objectData ? this.objectData : sourceRecord\n\t\t\t\tconst kind = this.kindOf(field)\n\t\t\t\tconst key = String(field.key || field.path || `${kind}-${index}`).replace(/[^\\w-]/g, '-')\n\t\t\t\tconst value = this.entryValue(record, field, kind)\n\t\t\t\tif (value === '' && (hideEmpty || kind === 'lead')) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (kind === 'row') {\n\t\t\t\t\tconst last = out[out.length - 1]\n\t\t\t\t\tconst row = { key, label: this.tr(field.label || ''), value, ...this.rowExtras(record, field, value) }\n\t\t\t\t\tif (last && last.kind === 'rows') {\n\t\t\t\t\t\tlast.rows.push(row)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tout.push({ kind: 'rows', key: `rows-${index}`, rows: [row] })\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst block = { kind, key, value }\n\t\t\t\tif (kind === 'lead') {\n\t\t\t\t\tblock.subtitle = this.partsValue(record, field.subtitle)\n\t\t\t\t\tblock.icon = typeof field.icon === 'string' ? field.icon : ''\n\t\t\t\t\tblock.initials = block.icon ? '' : this.initialsOf(readPath(record, field.initialsPath))\n\t\t\t\t\tblock.tone = ['success', 'info', 'warning', 'error'].includes(field.tone) ? field.tone : 'neutral'\n\t\t\t\t}\n\t\t\t\tif (kind === 'meter') {\n\t\t\t\t\tObject.assign(block, this.meterOf(record, field))\n\t\t\t\t}\n\t\t\t\tif (kind === 'title' && field.route) {\n\t\t\t\t\tblock.to = this.resolveRoute(field.route, record)\n\t\t\t\t}\n\t\t\t\tif (kind === 'pill') {\n\t\t\t\t\tconst raw = readPath(record, field.path)\n\t\t\t\t\tconst colours = field.variantMap || field.colorMap\n\t\t\t\t\t// A resolved pill looks its tone up by the referenced object's tone field,\n\t\t\t\t\t// then by the resolved label, then by the stored value.\n\t\t\t\t\tconst keys = field.resolve && typeof field.resolve === 'object'\n\t\t\t\t\t\t? [toneSpec(field) ? this.refLabels(raw, toneSpec(field))[0] : undefined, this.refLabels(raw, field)[0], raw]\n\t\t\t\t\t\t: [raw]\n\t\t\t\t\tconst hit = colours ? keys.find((candidate) => (typeof candidate === 'string' || typeof candidate === 'number') && Object.hasOwn(colours, candidate)) : undefined\n\t\t\t\t\tconst variant = hit !== undefined ? colours[hit] : field.variant\n\t\t\t\t\tblock.variant = isBadgeVariant(variant) ? variant : 'default'\n\t\t\t\t}\n\t\t\t\tout.push(block)\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The foot link, resolved, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tlink() {\n\t\t\tconst link = this.content?.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route) {\n\t\t\t\tconst to = this.resolveRoute(link.route, this.record)\n\t\t\t\treturn to ? { label: this.tr(link.label), to } : null\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\tconst href = this.resolveTemplate(link.href, this.record)\n\t\t\t\treturn href ? { label: this.tr(link.label), href } : null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The action under the facts, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\taction() {\n\t\t\treturn this.actionView(this.content?.action)\n\t\t},\n\n\t\t/**\n\t\t * The header action, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\theaderAction() {\n\t\t\treturn this.actionView(this.content?.headerAction)\n\t\t},\n\n\t\t/**\n\t\t * The card header's status pill, read from the record like a `pill`\n\t\t * entry, or null when it has no text.\n\t\t *\n\t\t * @return {{label: string, variant: string}|null}\n\t\t */\n\t\theaderPill() {\n\t\t\tconst pill = this.content?.headerPill\n\t\t\tif (!pill || typeof pill !== 'object' || !this.record) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = typeof pill.path === 'string' && pill.path !== '' ? readPath(this.record, pill.path) : undefined\n\t\t\tconst mapped = pill.map && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(pill.map, raw) ? pill.map[raw] : undefined\n\t\t\tconst text = mapped ?? pill.text ?? raw\n\t\t\tif (typeof text !== 'string' && typeof text !== 'number') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.tr(String(text))\n\t\t\tif (label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst colours = pill.variantMap || pill.colorMap\n\t\t\tconst hit = colours && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(colours, raw) ? colours[raw] : pill.variant\n\t\t\treturn { label, variant: isBadgeVariant(hit) ? hit : 'default' }\n\t\t},\n\n\t\t/**\n\t\t * The note band under the facts: its text translated, its NcNoteCard\n\t\t * type from `variant` (info by default), or null without text.\n\t\t *\n\t\t * @return {{text: string, type: string}|null}\n\t\t */\n\t\tnotice() {\n\t\t\tconst notice = this.content?.notice\n\t\t\tconst text = notice && typeof notice.text === 'string' ? this.tr(notice.text) : ''\n\t\t\tif (text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { text, type: ['success', 'warning', 'error'].includes(notice.variant) ? notice.variant : 'info' }\n\t\t},\n\n\t\t/** @return {string} The reveal button's text. */\n\t\tshowLabel() {\n\t\t\treturn t('nextcloud-vue', 'Show')\n\t\t},\n\n\t\t/**\n\t\t * The empty line.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content?.emptyText || 'Nothing to show yet')\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\t\t},\n\n\t\trecord: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadLabels()\n\t\t\t},\n\t\t},\n\n\t\theaderAction: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\n\t\theaderPill: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The kind of a field entry.\n\t\t *\n\t\t * @param {object} field The entry.\n\t\t * @return {string} `row`, `title`, `line` or `pill`.\n\t\t */\n\t\tkindOf(field) {\n\t\t\tif (KINDS.includes(field.as)) {\n\t\t\t\treturn field.as\n\t\t\t}\n\t\t\treturn field.label || field.icon ? 'row' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * A meter entry's parts: the amount at `path` against the limit at\n\t\t * `limitPath` (or a constant `limit`), both formatted with the entry's\n\t\t * `format`. Without a positive numeric limit there is no bar, only the\n\t\t * amount. Past the limit the text says so, so the state never rests on\n\t\t * the bar's colour alone.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {{label: string, value: string, amount: number|null, limit: number|null, percent: number, over: boolean}}\n\t\t * @spec openspec/changes/facts-meter-row/specs/facts-meter-row/spec.md#requirement-a-facts-entry-can-be-a-meter\n\t\t */\n\t\tmeterOf(record, field) {\n\t\t\tconst amount = Number(readPath(record, field.path))\n\t\t\tconst rawLimit = typeof field.limitPath === 'string' && field.limitPath !== '' ? readPath(record, field.limitPath) : field.limit\n\t\t\tconst limit = rawLimit === null || rawLimit === undefined || rawLimit === '' ? NaN : Number(rawLimit)\n\t\t\tconst label = this.tr(field.label || '')\n\t\t\tconst value = this.valueOf(record, field)\n\t\t\tif (!Number.isFinite(amount) || !Number.isFinite(limit) || limit <= 0) {\n\t\t\t\treturn { label, value, amount: Number.isFinite(amount) ? amount : null, limit: null, percent: 0, over: false }\n\t\t\t}\n\t\t\tconst over = amount > limit\n\t\t\tconst limitText = this.format(limit, field, record)\n\t\t\treturn {\n\t\t\t\tlabel,\n\t\t\t\tvalue: over\n\t\t\t\t\t? t('nextcloud-vue', '{value} of {limit}, over the limit', { value, limit: limitText })\n\t\t\t\t\t: t('nextcloud-vue', '{value} of {limit}', { value, limit: limitText }),\n\n\t\t\t\tamount,\n\t\t\t\tlimit,\n\t\t\t\tpercent: Math.max(0, Math.min(100, (amount / limit) * 100)),\n\t\t\t\tover,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The display text of an entry of any kind: a lead's title, the parts of\n\t\t * a line or text, a constant `text`, else the value at `path`.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} kind The entry's kind.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\tentryValue(record, field, kind) {\n\t\t\tif (kind === 'lead') {\n\t\t\t\treturn this.partsValue(record, field.title)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text' || kind === 'row') && Array.isArray(field.parts)) {\n\t\t\t\treturn this.lineValue(record, field)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {\n\t\t\t\treturn this.tr(field.text)\n\t\t\t}\n\t\t\treturn this.valueOf(record, field)\n\t\t},\n\n\t\t/**\n\t\t * A lead's title or subtitle: a path, a part spec, or an array of parts\n\t\t * joined as a line joins them.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {string|object|Array} spec The declaration.\n\t\t * @return {string}\n\t\t */\n\t\tpartsValue(record, spec) {\n\t\t\tif (typeof spec === 'string' && spec !== '') {\n\t\t\t\treturn this.valueOf(record, { path: spec })\n\t\t\t}\n\t\t\tif (Array.isArray(spec)) {\n\t\t\t\treturn this.lineValue(record, { parts: spec })\n\t\t\t}\n\t\t\tif (spec && typeof spec === 'object') {\n\t\t\t\treturn this.lineValue(record, { parts: [spec] })\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The initials of a name: \"Sanne de Vries\" reads \"SV\".\n\t\t *\n\t\t * @param {unknown} name The name.\n\t\t * @return {string}\n\t\t */\n\t\tinitialsOf(name) {\n\t\t\tif (typeof name !== 'string' || name.trim() === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst words = name.trim().split(/\\s+/).filter((word) => /^\\p{Lu}/u.test(word))\n\t\t\tconst picked = words.length ? words : name.trim().split(/\\s+/)\n\t\t\tconst first = picked[0].charAt(0)\n\t\t\tconst last = picked.length > 1 ? picked[picked.length - 1].charAt(0) : ''\n\t\t\treturn (first + last).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * What a row adds to its value: an icon, a link for the email, phone\n\t\t * and url formats, the masked text, and the copy and reveal buttons.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} value The display text.\n\t\t * @return {object}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trowExtras(record, field, value) {\n\t\t\tconst extras = {\n\t\t\t\ticon: typeof field.icon === 'string' ? field.icon : '',\n\t\t\t\tpath: field.path || '',\n\t\t\t\traw: readPath(record, field.path),\n\t\t\t\tcopy: field.copy === true && value !== '',\n\t\t\t\treveal: false,\n\t\t\t\tmasked: '',\n\t\t\t\thref: '',\n\t\t\t\texternal: false,\n\t\t\t}\n\t\t\tif (value === '') {\n\t\t\t\treturn extras\n\t\t\t}\n\t\t\tif (field.format === 'email') {\n\t\t\t\textras.href = `mailto:${value}`\n\t\t\t} else if (field.format === 'phone') {\n\t\t\t\textras.href = `tel:${value.replace(/[^\\d+]/g, '')}`\n\t\t\t} else if (field.format === 'url') {\n\t\t\t\textras.href = /^https?:\\/\\//i.test(value) ? value : `https://${value}`\n\t\t\t\textras.external = true\n\t\t\t} else if (field.format === 'masked') {\n\t\t\t\textras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value\n\t\t\t\textras.reveal = field.reveal === true && value.length > 4\n\t\t\t}\n\t\t\treturn extras\n\t\t},\n\n\t\t/**\n\t\t * Show a masked value, and say so: the `reveal` event and the event-bus\n\t\t * channel {@link FACTS_REVEAL_CHANNEL}, so an app can log who saw it.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\treveal(row) {\n\t\t\tthis.revealed = { ...this.revealed, [row.key]: true }\n\t\t\tconst detail = { path: row.path }\n\t\t\t/**\n\t\t\t * @event reveal Emitted when the reader shows a masked value.\n\t\t\t * @type {{path: string}} The path of the shown field.\n\t\t\t */\n\t\t\tthis.$emit('reveal', detail)\n\t\t\temit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null })\n\t\t},\n\n\t\t/**\n\t\t * Copy a row's value to the clipboard.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync copy(row) {\n\t\t\tconst text = row.raw === null || row.raw === undefined || typeof row.raw === 'object' ? row.value : String(row.raw)\n\t\t\ttry {\n\t\t\t\tawait navigator.clipboard.writeText(text)\n\t\t\t\tthis.copied = row.key\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (this.copied === row.key) {\n\t\t\t\t\t\tthis.copied = ''\n\t\t\t\t\t}\n\t\t\t\t}, 2000)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] could not copy:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The copy button's accessible name.\n\t\t */\n\t\tcopyLabel(row) {\n\t\t\treturn this.copied === row.key\n\t\t\t\t? t('nextcloud-vue', 'Copied')\n\t\t\t\t: t('nextcloud-vue', 'Copy {label}', { label: row.label || row.value })\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The reveal button's accessible name.\n\t\t */\n\t\trevealLabel(row) {\n\t\t\treturn t('nextcloud-vue', 'Show {label}', { label: row.label || '' }).trim()\n\t\t},\n\n\t\t/**\n\t\t * The drawn part of an action declaration.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {{label: string, icon: string}|null}\n\t\t */\n\t\tactionView(action) {\n\t\t\tif (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { label: this.tr(action.label), icon: typeof action.icon === 'string' ? action.icon : '' }\n\t\t},\n\n\t\t/**\n\t\t * Dispatch an action with the record's tokens resolved (`@objectId`,\n\t\t * `@object.<field>`), through the page's dispatcher when there is one.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {unknown}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trunAction(action) {\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(resolved)\n\t\t\t}\n\t\t\treturn dispatchAction(resolved, { router: this.$router || null })\n\t\t},\n\n\t\t/** Tell the card around the facts what its header shows (the header action). */\n\t\temitHeader() {\n\t\t\tconst view = this.headerAction\n\t\t\t/**\n\t\t\t * @event header Emitted with the card header's parts: the header action, if any.\n\t\t\t * @type {{link: null, note: string, action: (object|null), pill: (object|null)}} `action` is `{ label, icon, run }`, `pill` is `{ label, variant }`.\n\t\t\t */\n\t\t\tthis.$emit('header', {\n\t\t\t\tlink: null,\n\t\t\t\tnote: '',\n\t\t\t\taction: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,\n\t\t\t\tpill: this.headerPill,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The text of a reference: the labels of the objects its id(s) name, joined\n\t\t * by \", \". An id whose label is not known yet (or cannot be read) adds nothing.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids (or of `{ id }` objects).\n\t\t * @param {object} spec `{ resolve: { register, schema, labelField? }, itemPath? }`.\n\t\t * @return {string}\n\t\t */\n\t\trefText(raw, spec) {\n\t\t\treturn this.refLabels(raw, spec)\n\t\t\t\t.map((label) => (spec.map && Object.hasOwn(spec.map, label) ? this.tr(spec.map[label]) : label))\n\t\t\t\t.join(', ')\n\t\t},\n\n\t\t/**\n\t\t * The labels of a reference as read, before `map` is applied: the key a\n\t\t * pill's `variantMap` and `colorMap` look up.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids.\n\t\t * @param {object} spec `{ resolve, itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefLabels(raw, spec) {\n\t\t\treturn this.refIds(raw, spec)\n\t\t\t\t.map((id) => this.labels[refKey(spec.resolve, id)])\n\t\t\t\t.filter((label) => typeof label === 'string' && label !== '')\n\t\t},\n\n\t\t/**\n\t\t * The ids a reference value holds.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefIds(raw, spec) {\n\t\t\treturn (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t.map((item) => (item && typeof item === 'object' ? (spec.itemPath ? readPath(item, spec.itemPath) : (item.id ?? item.uuid)) : item))\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')\n\t\t\t\t.map(String)\n\t\t},\n\n\t\t/**\n\t\t * Every entry or part that carries `resolve`, with the record's value at its path.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @return {Array<{spec: object, raw: unknown}>}\n\t\t */\n\t\trefSpecs(record) {\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst specs = []\n\t\t\tconst visit = (spec) => {\n\t\t\t\tif (!spec || typeof spec !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (spec.resolve && typeof spec.resolve === 'object' && spec.path) {\n\t\t\t\t\tspecs.push({ spec, raw: readPath(record, spec.path) })\n\t\t\t\t\tif (toneSpec(spec)) {\n\t\t\t\t\t\tspecs.push({ spec: toneSpec(spec), raw: readPath(record, spec.path) })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst nested = [...(Array.isArray(spec.parts) ? spec.parts : []), ...(Array.isArray(spec.title) ? spec.title : [spec.title]), ...(Array.isArray(spec.subtitle) ? spec.subtitle : [spec.subtitle])]\n\t\t\t\tnested.forEach(visit)\n\t\t\t}\n\t\t\tfields.forEach(visit)\n\t\t\treturn specs\n\t\t},\n\n\t\t/**\n\t\t * Read the labels of the referenced objects, one batched request per\n\t\t * register and schema, and keep them in `labels`.\n\t\t *\n\t\t * @param {object|null} record The object shown (defaults to the current record).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLabels(record = this.record) {\n\t\t\tif (!record) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst groups = new Map()\n\t\t\tfor (const { spec, raw } of this.refSpecs(record)) {\n\t\t\t\tconst { register, schema, labelField } = spec.resolve\n\t\t\t\tif (!register || !schema) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst groupKey = `${register}/${schema}/${labelField || ''}`\n\t\t\t\tconst group = groups.get(groupKey) || { resolve: spec.resolve, ids: new Set() }\n\t\t\t\tthis.refIds(raw, spec).forEach((id) => group.ids.add(id))\n\t\t\t\tgroups.set(groupKey, group)\n\t\t\t}\n\t\t\tif (groups.size === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolver = sharedRefLabels()\n\t\t\tconst next = { ...this.labels }\n\t\t\tawait Promise.all([...groups.values()].map(async ({ resolve, ids }) => {\n\t\t\t\tconst found = await resolver.resolve(resolve.register, resolve.schema, [...ids], resolve.labelField)\n\t\t\t\tObject.entries(found).forEach(([id, label]) => {\n\t\t\t\t\tnext[refKey(resolve, id)] = label\n\t\t\t\t})\n\t\t\t}))\n\t\t\tthis.labels = next\n\t\t},\n\n\t\t/**\n\t\t * Read the source object.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst source = this.resolvedSource\n\t\t\tthis.error = false\n\t\t\tif (!source) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst url = this.sourceUrl(source)\n\t\t\tif (!url) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url\n\t\t\t\tconst response = await axios.get(full)\n\t\t\t\tconst data = response && response.data\n\t\t\t\tthis.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] the source could not be read:', e?.message || e)\n\t\t\t\tthis.fetched = null\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The URL of a resolved source, or '' when it cannot be read yet.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @return {string}\n\t\t */\n\t\tsourceUrl(source) {\n\t\t\tif (typeof source.url === 'string') {\n\t\t\t\treturn source.url\n\t\t\t}\n\t\t\tif (hasUnresolvedDeepTokens(source)) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst { register, schema, id } = source\n\t\t\tif (!register || !schema || id === undefined || id === null || id === '' || typeof id === 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\n\t\t/**\n\t\t * The display text of one field.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tvalueOf(record, field) {\n\t\t\tconst raw = readPath(record, field.path)\n\t\t\treturn this.format(raw, field, record)\n\t\t},\n\n\t\t/**\n\t\t * The text of a `line` entry that joins several parts.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tlineValue(record, field) {\n\t\t\tconst separator = typeof field.separator === 'string' ? field.separator : ' · '\n\t\t\treturn field.parts\n\t\t\t\t.map((part) => {\n\t\t\t\t\tconst spec = typeof part === 'string' ? { path: part } : (part || {})\n\t\t\t\t\t// A constant part (`{ text }`): \"Handelsregister\" after the legal form.\n\t\t\t\t\tif (!spec.path && typeof spec.text === 'string') {\n\t\t\t\t\t\treturn this.tr(spec.text)\n\t\t\t\t\t}\n\t\t\t\t\t// A group of parts with its own separator (\"Parkstraat 24\" inside\n\t\t\t\t\t// \"Parkstraat 24, 3421 AB Zuiddrecht\").\n\t\t\t\t\tif (!spec.path && Array.isArray(spec.parts)) {\n\t\t\t\t\t\treturn this.lineValue(record, spec)\n\t\t\t\t\t}\n\t\t\t\t\tconst value = this.format(readPath(record, spec.path), spec, record)\n\t\t\t\t\tif (value === '') {\n\t\t\t\t\t\treturn ''\n\t\t\t\t\t}\n\t\t\t\t\treturn spec.label ? `${this.tr(spec.label)} ${value}` : value\n\t\t\t\t})\n\t\t\t\t.filter((value) => value !== '')\n\t\t\t\t.join(separator)\n\t\t},\n\n\t\t/**\n\t\t * Format a raw value.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ format?, map?, itemPath?, currency?, currencyField?, decimals? }`.\n\t\t * @param {object|null} [record] The object shown, which `currencyField` reads.\n\t\t * @return {string} The text, '' when empty.\n\t\t * @spec openspec/changes/facts-currency-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-money-amount\n\t\t * @spec openspec/changes/facts-user-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-user-by-name\n\t\t */\n\t\tformat(raw, spec, record = null) {\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (spec.resolve && typeof spec.resolve === 'object') {\n\t\t\t\treturn this.refText(raw, spec)\n\t\t\t}\n\t\t\tconst format = spec.format || 'text'\n\t\t\tif (format === 'list' || Array.isArray(raw)) {\n\t\t\t\tconst items = (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t\t.map((item) => (spec.itemPath && item && typeof item === 'object' ? readPath(item, spec.itemPath) : item))\n\t\t\t\t\t.map((item) => this.format(item, { ...spec, format: 'text' }))\n\t\t\t\t\t.filter((item) => item !== '')\n\t\t\t\treturn items.join(', ')\n\t\t\t}\n\t\t\tif (format === 'date' || format === 'date-long') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\tif (date === null) {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\t// A year far from now forces the long form, so date-long reuses the board formatter.\n\t\t\t\treturn format === 'date-long'\n\t\t\t\t\t? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))\n\t\t\t\t\t: formatBoardDate(date)\n\t\t\t}\n\t\t\tif (format === 'bytes') {\n\t\t\t\tconst bytes = Number(raw)\n\t\t\t\treturn Number.isFinite(bytes) && bytes >= 0 ? formatFileSize(bytes) : ''\n\t\t\t}\n\t\t\tif (format === 'user') {\n\t\t\t\treturn userDisplayName(raw)\n\t\t\t}\n\t\t\tif (format === 'currency') {\n\t\t\t\tconst money = formatMoney(raw, { currency: spec.currency, currencyField: spec.currencyField, decimals: spec.decimals }, record, unwrapAppConfig(this.cnAppConfig))\n\t\t\t\tif (money !== '') {\n\t\t\t\t\treturn money\n\t\t\t\t}\n\t\t\t\t// Not a number: shown as written, never as \"[object Object]\".\n\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t}\n\t\t\tif (typeof raw === 'object') {\n\t\t\t\tconst label = raw.title || raw.name || raw.label || raw.displayName\n\t\t\t\treturn typeof label === 'string' ? label : ''\n\t\t\t}\n\t\t\tif (typeof raw === 'boolean') {\n\t\t\t\tconst mapped = spec.map && spec.map[String(raw)]\n\t\t\t\treturn mapped ? this.tr(mapped) : this.tr(raw ? 'Yes' : 'No')\n\t\t\t}\n\t\t\tif (spec.map && Object.hasOwn(spec.map, String(raw))) {\n\t\t\t\treturn this.tr(spec.map[String(raw)])\n\t\t\t}\n\t\t\treturn String(raw)\n\t\t},\n\n\t\t/**\n\t\t * Replace `@source.<field>` / `@source.id` and the shared tokens in a string.\n\t\t *\n\t\t * @param {string} value The template.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {string} The value, '' when a token stays unresolved.\n\t\t */\n\t\tresolveTemplate(value, record) {\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tlet unresolved = false\n\t\t\tconst replaced = value.replace(/@source\\.([\\w.@]+)/g, (match, path) => {\n\t\t\t\tconst read = record ? (path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)) : undefined\n\t\t\t\tif (read === undefined || read === null || read === '') {\n\t\t\t\t\tunresolved = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(read)\n\t\t\t})\n\t\t\tif (unresolved) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (replaced.charAt(0) === '@') {\n\t\t\t\tconst resolved = resolveDeepTokens(replaced, this.tokenCtx)\n\t\t\t\treturn typeof resolved === 'string' && resolved.charAt(0) === '@' ? '' : String(resolved)\n\t\t\t}\n\t\t\treturn replaced\n\t\t},\n\n\t\t/**\n\t\t * Resolve a route declaration (`{ name, params?, query? }`).\n\t\t *\n\t\t * @param {object} route The declaration.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {object|null} The route, or null when a param stays unresolved.\n\t\t */\n\t\tresolveRoute(route, record) {\n\t\t\tif (!route || typeof route !== 'object' || !route.name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst out = { name: route.name }\n\t\t\tfor (const key of ['params', 'query']) {\n\t\t\t\tif (route[key] && typeof route[key] === 'object') {\n\t\t\t\t\tout[key] = {}\n\t\t\t\t\tfor (const [name, value] of Object.entries(route[key])) {\n\t\t\t\t\t\tconst resolved = this.resolveTemplate(value, record)\n\t\t\t\t\t\tif (resolved === '') {\n\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t}\n\t\t\t\t\t\tout[key][name] = resolved\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-facts-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tmin-width: 0;\n}\n\n/* `padding: 0`: Nextcloud's core stylesheet pads every `dl` 12px at the top\n and bottom, which put a side card's facts 24px under its title, against\n the board's 12 (DqZaak \"Behandeling\", DqZaakDocumenten). */\n.cn-facts-widget__rows {\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\tgap: 8px 16px;\n\tmargin: 0;\n\tpadding: 0;\n\tfont-size: 14px;\n}\n\n/* Each row is a grid of its own children: the wrapper takes no box. */\n.cn-facts-widget__row {\n\tdisplay: contents;\n}\n\n/* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which\n pushed the label to the right of its column and added 12px around both. The\n class is doubled so these resets outrank that rule, and a dossiq-style\n `:deep()` override in an app is not needed. */\n.cn-facts-widget__label.cn-facts-widget__label {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__value.cn-facts-widget__value {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tfont-weight: 500;\n\tmin-width: 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-facts-widget__title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__line {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__meter {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tfont-size: 14px;\n}\n\n.cn-facts-widget__meter-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 16px;\n}\n\n.cn-facts-widget__meter-label {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__meter-value {\n\tfont-weight: 500;\n\ttext-align: end;\n}\n\n.cn-facts-widget__meter-track {\n\theight: 6px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-darker);\n\toverflow: hidden;\n}\n\n.cn-facts-widget__meter-bar {\n\tdisplay: block;\n\theight: 100%;\n\tborder-radius: inherit;\n\tbackground: var(--color-primary-element);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-value {\n\tcolor: var(--color-error-text);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-bar {\n\tbackground: var(--color-error);\n}\n\n.cn-facts-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__label .hidden-visually {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n\n.cn-facts-widget__label--icon {\n\talign-items: center;\n\tdisplay: flex;\n}\n\n.cn-facts-widget__value-link {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-facts-widget__inline-button,\n.cn-facts-widget__icon-button,\n.cn-facts-widget__action {\n\talign-items: center;\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n}\n\n.cn-facts-widget__inline-button {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: 16px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tgap: 4px;\n\tmargin-inline-start: 8px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n}\n\n.cn-facts-widget__icon-button {\n\tbackground: transparent;\n\tborder: 0;\n\tborder-radius: 6px;\n\tcolor: var(--color-text-maxcontrast);\n\tmargin-inline-start: 4px;\n\tpadding: 2px;\n\tvertical-align: middle;\n}\n\n.cn-facts-widget__icon-button:hover,\n.cn-facts-widget__icon-button:focus-visible {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__text {\n\tcolor: var(--color-main-text);\n\tfont-size: 15px;\n}\n\n/* A lead: an icon tile or initials beside a bold title and a muted subtitle. */\n.cn-facts-widget__lead {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-facts-widget__lead-tile {\n\talign-items: center;\n\tbackground: var(--color-background-dark);\n\tborder-radius: 8px;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 14px;\n\tfont-weight: 700;\n\theight: 40px;\n\tjustify-content: center;\n\twidth: 40px;\n}\n\n.cn-facts-widget__lead-tile--success {\n\tbackground: color-mix(in srgb, var(--color-success) 14%, var(--color-main-background));\n\tborder-radius: 50%;\n\tcolor: var(--color-success-text, var(--color-success));\n}\n\n.cn-facts-widget__lead-tile--info {\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n}\n\n.cn-facts-widget__lead-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__lead-title {\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__lead-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-facts-widget__action {\n\talign-self: flex-start;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n}\n\n.cn-facts-widget__action:hover,\n.cn-facts-widget__action:focus-visible {\n\tbackground: var(--color-primary-element-light-hover, var(--color-primary-element-light));\n}\n\n.cn-facts-widget__notice {\n\tmargin: 4px 0 0;\n}\n\n.cn-facts-widget__link {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n/* `columns: 2` (board DqContact): two columns, the label above the value, a\n hairline under each fact; one column on a phone. */\n.cn-facts-widget__rows--columns {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\tgap: 14px 24px;\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__rows--columns .cn-facts-widget__row {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 3px;\n\tmin-width: 0;\n\tpadding-bottom: 10px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-facts-widget__rows--columns .cn-facts-widget__label.cn-facts-widget__label {\n\tfont-size: 13px;\n}\n\n@media (max-width: 640px) {\n\t.cn-facts-widget__rows--columns {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createVNode","_Fragment","_renderList","_normalizeClass","_createElementVNode","_createBlock","_createTextVNode","_resolveDynamicComponent","_normalizeStyle"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnFeaturesAndRoadmapView.vue2.js';
2
2
  import { render } from './CnFeaturesAndRoadmapView.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-3db485a4";
5
+ script.__scopeId = "data-v-2dd578ca";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -346,7 +346,7 @@ var script = {
346
346
  * @return {object}
347
347
  */
348
348
  boardOptions() {
349
- const keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton'];
349
+ const keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton', 'actionsMenu'];
350
350
  const options = {};
351
351
  for (const key of keys) {
352
352
  if (this.board && this.board[key] !== undefined && this.board[key] !== null) {
@@ -1 +1 @@
1
- {"version":3,"file":"CnFeaturesAndRoadmapView.vue2.js","sources":["../../../../src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-features-and-roadmap-view\">\n\t\t<div v-if=\"disabled\" class=\"cn-features-and-roadmap-view__disabled\">\n\t\t\t<NcEmptyContent :name=\"disabledTitle\" :description=\"disabledDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LockOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<CnFeaturesBoard\n\t\t\tv-else-if=\"isBoardLook\"\n\t\t\t:features=\"features\"\n\t\t\t:categories=\"categories\"\n\t\t\t:repo=\"repo\"\n\t\t\t:title=\"title\"\n\t\t\t:appVersion=\"appVersion\"\n\t\t\t:documentationUrl=\"resolvedDocumentationUrl\"\n\t\t\t:suggestUrl=\"resolvedSuggestUrl\"\n\t\t\t:openbuiltUrl=\"resolvedOpenbuiltUrl\"\n\t\t\t:llmSkillsUrl=\"resolvedLlmSkillsUrl\"\n\t\t\tv-bind=\"boardOptions\"\n\t\t\t@support=\"openSupportDialog\" />\n\t\t<template v-else>\n\t\t\t<header class=\"cn-features-and-roadmap-view__header\">\n\t\t\t\t<h2 class=\"cn-features-and-roadmap-view__title\">\n\t\t\t\t\t{{ headerTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-features-and-roadmap-view__actions\">\n\t\t\t\t\t<NcButton @click=\"toggleView\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<TableSearch v-if=\"nextView === 'capabilities'\" :size=\"20\" />\n\t\t\t\t\t\t\t<RoadVariant v-else-if=\"nextView === 'roadmap'\" :size=\"20\" />\n\t\t\t\t\t\t\t<FormatListBulleted v-else :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ toggleLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<!-- Direct link to the forge's feature-request issue form\n\t\t\t\t\t (team decision 2026-09-04: the in-product suggestion\n\t\t\t\t\t modal is gone — the forge is where the conversation\n\t\t\t\t\t happens, in English). -->\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"resolvedSuggestUrl\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t:href=\"resolvedSuggestUrl\"\n\t\t\t\t\t\ttarget=\"_blank\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ suggestLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t<NcNoteCard\n\t\t\t\tv-if=\"resolvedDocumentationUrl\"\n\t\t\t\ttype=\"info\"\n\t\t\t\tclass=\"cn-features-and-roadmap-view__docs-note\">\n\t\t\t\t{{ docsNoteLeading }}\n\t\t\t\t<a\n\t\t\t\t\t:href=\"resolvedDocumentationUrl\"\n\t\t\t\t\t:target=\"documentationUrlIsExternal ? '_blank' : null\"\n\t\t\t\t\t:rel=\"documentationUrlIsExternal ? 'noopener noreferrer' : null\">{{ docsNoteLinkLabel }}</a>\n\t\t\t\t{{ docsNoteTrailing }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<main class=\"cn-features-and-roadmap-view__panel\">\n\t\t\t\t<CnFeaturesTab v-if=\"activeView === 'features'\" :features=\"features\" />\n\t\t\t\t<CnCapabilityTable\n\t\t\t\t\tv-else-if=\"activeView === 'capabilities'\"\n\t\t\t\t\t:comparison=\"capabilityComparison\" />\n\t\t\t\t<CnRoadmapTab v-else :repo=\"repo\" />\n\t\t\t</main>\n\t\t</template>\n\t\t<CnSupportDialog\n\t\t\tv-if=\"showSupportDialog && !disabled\"\n\t\t\t:appName=\"resolvedAppName\"\n\t\t\t:appSlug=\"resolvedAppSlug\"\n\t\t\t:appStoreUrl=\"resolvedAppStoreUrl\"\n\t\t\t:featureRequestUrl=\"resolvedFeatureRequestUrl\"\n\t\t\t:donateUrl=\"donateUrl\"\n\t\t\t:supportUrl=\"supportUrl\"\n\t\t\t:founderName=\"founderName\"\n\t\t\t:founderTitle=\"founderTitle\"\n\t\t\t@close=\"showSupportDialog = false\" />\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnFeaturesAndRoadmapView — route-level container for the Features &\n * Roadmap surface. Header carries the view-toggle and the primary\n * Suggest-feature CTA. Body is a card grid (Features OR Roadmap).\n *\n * The right-edge sidebar is hoisted to NcContent level via the same\n * `cnIndexSidebarConfig` provide mechanism CnIndexPage uses for its\n * CnIndexSidebar — that's the only place where Nextcloud's right-edge\n * sidebar slot renders correctly (next to NcAppContent, not inside it).\n * When mounted under CnAppRoot the view publishes\n * `CnFeaturesAndRoadmapSidebar` + props into the holder on mounted()\n * and clears it on beforeUnmount(). Sidebar carries four pitch\n * sections: Suggest, OpenBuilt, LLM, Support. Suggest renders as an\n * anchor to the forge's feature-request issue form (`resolvedSuggestUrl`\n * is always passed); Support emits `@support` (forwarded to a\n * freshly-mounted `CnSupportDialog`).\n *\n * Sidebar link targets are overridable via the `openbuiltUrl` and\n * `llmSkillsUrl` props. Defaults: in-instance `/apps/openbuilt` (via\n * `generateUrl`) and `https://docs.conduction.nl/ai-skills`.\n *\n * Spec: features-roadmap-component — Requirement \"CnFeaturesAndRoadmapView\".\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcEmptyContent, NcNoteCard } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport FormatListBulleted from 'vue-material-design-icons/FormatListBulleted.vue'\nimport LockOutline from 'vue-material-design-icons/LockOutline.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport RoadVariant from 'vue-material-design-icons/RoadVariant.vue'\nimport TableSearch from 'vue-material-design-icons/TableSearch.vue'\nimport CnCapabilityTable from '../CnCapabilityTable/CnCapabilityTable.vue'\nimport CnFeaturesAndRoadmapSidebar from '../CnFeaturesAndRoadmapSidebar/CnFeaturesAndRoadmapSidebar.vue'\nimport CnFeaturesBoard from '../CnFeaturesBoard/CnFeaturesBoard.vue'\nimport CnFeaturesTab from '../CnFeaturesTab/CnFeaturesTab.vue'\nimport CnRoadmapTab from '../CnRoadmapTab/CnRoadmapTab.vue'\nimport CnSupportDialog from '../CnSupportDialog/CnSupportDialog.vue'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { warnCapabilityComparisonDeprecated } from '../../utils/capabilityComparison.js'\nimport { buildFeatureRequestUrl, DEFAULT_FORGE } from '../../utils/forge.js'\n\nconst DEFAULT_OPENBUILT_PATH = '/apps/openbuilt'\nconst DEFAULT_LLM_SKILLS_URL = 'https://docs.conduction.nl/ai-skills'\nconst DEFAULT_DONATE_URL = 'https://github.com/sponsors/ConductionNL'\nconst DEFAULT_SUPPORT_URL = 'https://www.conduction.nl/support'\nconst DEFAULT_FOUNDER_NAME = 'Ruben van der Linde'\nconst DEFAULT_FOUNDER_TITLE = 'a founder of Conduction'\n\nexport default {\n\tname: 'CnFeaturesAndRoadmapView',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcNoteCard,\n\t\tFormatListBulleted,\n\t\tLockOutline,\n\t\tPlus,\n\t\tRoadVariant,\n\t\tTableSearch,\n\t\tCnCapabilityTable,\n\t\tCnFeaturesBoard,\n\t\tCnFeaturesTab,\n\t\tCnRoadmapTab,\n\t\tCnSupportDialog,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Set to `true` by CnAppRoot so descendants know the host will\n\t\t * mount a hoisted sidebar at NcContent level (see CnAppRoot.vue\n\t\t * `cnHostsIndexSidebar: true`). When `false` (no CnAppRoot\n\t\t * ancestor) the view skips publishing — there's no inline\n\t\t * fallback for this surface.\n\t\t */\n\t\tcnHostsIndexSidebar: { default: false },\n\t\t/**\n\t\t * Reactive holder published by CnAppRoot that backs the\n\t\t * hoisted-sidebar render in NcContent. Set value to\n\t\t * `{component, props, listeners}` to mount; set to `null` to\n\t\t * unmount. Same holder CnIndexPage uses.\n\t\t */\n\t\tcnIndexSidebarConfig: { default: () => ({ value: null }) },\n\t\t/** The app's look, provided by CnAppRoot (`nextcloud` or `board`). */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * `<owner>/<repo>` of the app's repository on the forge.\n\t\t */\n\t\trepo: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Target forge the feature-request deep-links are built against\n\t\t * (the Suggest CTA and the support dialog's feature-request URL).\n\t\t * Set `{type: 'github'}` (optionally with `baseUrl`) to switch\n\t\t * forge.\n\t\t *\n\t\t * @type {{type: 'codeberg'|'forgejo'|'gitea'|'github', baseUrl?: string}}\n\t\t */\n\t\tforge: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({ ...DEFAULT_FORGE }),\n\t\t},\n\n\t\t/**\n\t\t * Build-time feature manifest (alphabetical list rendered by CnFeaturesTab).\n\t\t *\n\t\t * @type {Array<{slug: string, title: string, summary: string, docsUrl: string}>}\n\t\t */\n\t\tfeatures: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Feature category key to caption, in the order the board look shows\n\t\t * the groups. A feature names its group with `category`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Board look options, passed to CnFeaturesBoard: `layout` (`cards` or\n\t\t * `rows`), `tabs`, `filter` (`chips` or `segmented`), `statusLabels`,\n\t\t * `next`, `issueLabels`, `subtitle`, `notice`, `noticeLink`,\n\t\t * `summaries`, `documentationButton` and `suggestButton`. Ignored in the default look.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tboard: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Page title in the board look. Empty reads \"Features and roadmap\".\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The app version, for `{appVersion}` in the board look's subtitle.\n\t\t */\n\t\tappVersion: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Admin opt-out flag — when true the entire view collapses to a\n\t\t * single \"disabled by admin\" empty state.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional capability comparison. When supplied, the header toggle\n\t\t * gains a third stop that renders `CnCapabilityTable`: a searchable\n\t\t * table of the rows, grouped by feature or by area, with a\n\t\t * provided-by column naming which app or platform delivers each one.\n\t\t *\n\t\t * Shape: `{systems, areas, features?, providers?, capabilities}`. The\n\t\t * rows carry the competitor ratings they already carry, plus the\n\t\t * optional `provider`, `providerHow`, `feature` and\n\t\t * `featureConfidence` fields.\n\t\t *\n\t\t * When null (default) the toggle keeps its two stops and the view\n\t\t * renders exactly as it does for every app that passes nothing.\n\t\t *\n\t\t * Deprecated: publish the comparison on the app's docs site and link to it (as dossiq #3312 and pipelinq #2195 did). Still rendered; removal waits for a major version.\n\t\t * A development build warns once when it is set.\n\t\t *\n\t\t * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated\n\t\t * @type {object|null}\n\t\t */\n\t\tcapabilityComparison: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Override the OpenBuilt sidebar CTA target. Pass an absolute URL or\n\t\t * a Nextcloud-relative path. When unset, defaults to the in-instance\n\t\t * OpenBuilt route at `/apps/openbuilt` (resolved via `generateUrl`).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\topenbuiltUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Override the LLM-skills sidebar CTA target. Defaults to\n\t\t * `https://docs.conduction.nl/ai-skills`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tllmSkillsUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional override for the Suggest CTA inside the sidebar. When\n\t\t * set the CTA renders as an anchor pointing at this URL —\n\t\t * appropriate when the app routes feature suggestions through a\n\t\t * public form, a Discord channel, or any non-forge target. When\n\t\t * empty (default) the CTA links the forge's feature-request\n\t\t * issue form derived from `repo` + `forge`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsuggestUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional URL of the app's public documentation site. When set,\n\t\t * an info banner is rendered above the card grid pointing users at\n\t\t * `<docs>` for full technical + user docs. Per-app — pipelinq\n\t\t * passes `https://pipelinq.conduction.nl`, decidesk passes its\n\t\t * own, etc. When empty (default) no banner renders.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Display name of the host app, used as the title of the bundled\n\t\t * `CnSupportDialog` and interpolated into its body copy. When\n\t\t * empty (default) the View derives a humanised label from `repo`\n\t\t * — `ConductionNL/openregister` becomes `Openregister` — which is\n\t\t * fine for stock apps and overridable for camelCased names.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Kebab-case host-app id passed to `CnSupportDialog` (its\n\t\t * localStorage namespace). When empty (default) the View derives\n\t\t * it from `repo` — second segment lower-cased.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappSlug: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Nextcloud App Store listing URL for the host app, threaded\n\t\t * through to `CnSupportDialog`. When empty (default) the View\n\t\t * derives `https://apps.nextcloud.com/apps/{appSlug}`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappStoreUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * URL the \"Suggest a feature\" CTA inside `CnSupportDialog` opens.\n\t\t * When empty (default) the View derives\n\t\t * `<forge-base>/{repo}/issues/new` from the `forge` prop. This is\n\t\t * intentionally separate from the prop-of-the-same-name on the\n\t\t * sidebar's Suggest CTA — that one opens the modal-driven flow;\n\t\t * this one is the \"open on the forge\" fallback used by the support\n\t\t * dialog, which is meant for the casual visitor rather than the\n\t\t * existing suggest-feature loop.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tfeatureRequestUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Donate-CTA URL passed to `CnSupportDialog`. Defaults to\n\t\t * ConductionNL's GitHub Sponsors page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdonateUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_DONATE_URL,\n\t\t},\n\n\t\t/**\n\t\t * Business-support CTA URL passed to `CnSupportDialog`. Defaults\n\t\t * to the Conduction contact page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsupportUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_SUPPORT_URL,\n\t\t},\n\n\t\t/**\n\t\t * Name rendered in the handwritten signature of `CnSupportDialog`.\n\t\t * Defaults to Ruben van der Linde.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tfounderName: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_FOUNDER_NAME,\n\t\t},\n\n\t\t/**\n\t\t * Title shown after the handwritten signature in `CnSupportDialog`\n\t\t * (e.g. \"Founder\", \"Oprichter\").\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tfounderTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_FOUNDER_TITLE,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveView: 'features',\n\t\t\tshowSupportDialog: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the page is drawn in the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * The board options CnFeaturesBoard declares, so an unknown key in the\n\t\t * manifest never lands on its root element as an attribute.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tboardOptions() {\n\t\t\tconst keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton']\n\t\t\tconst options = {}\n\t\t\tfor (const key of keys) {\n\t\t\t\tif (this.board && this.board[key] !== undefined && this.board[key] !== null) {\n\t\t\t\t\toptions[key] = this.board[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The stops the header toggle cycles through. Without a capability\n\t\t * comparison this is the two-stop list every consumer has today, and\n\t\t * the toggle behaves exactly as it did.\n\t\t *\n\t\t * @return {Array<string>} The views, in cycle order.\n\t\t */\n\t\tavailableViews() {\n\t\t\treturn this.capabilityComparison\n\t\t\t\t? ['features', 'capabilities', 'roadmap']\n\t\t\t\t: ['features', 'roadmap']\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The view the toggle moves to next.\n\t\t */\n\t\tnextView() {\n\t\t\tconst views = this.availableViews\n\t\t\tconst index = views.indexOf(this.activeView)\n\t\t\treturn views[(index + 1) % views.length]\n\t\t},\n\n\t\theaderTitle() {\n\t\t\treturn this.viewTitle(this.activeView)\n\t\t},\n\n\t\ttoggleLabel() {\n\t\t\tif (this.nextView === 'capabilities') {\n\t\t\t\treturn t('nextcloud-vue', 'Show capabilities')\n\t\t\t}\n\t\t\treturn this.nextView === 'roadmap'\n\t\t\t\t? t('nextcloud-vue', 'Show roadmap')\n\t\t\t\t: t('nextcloud-vue', 'Show features')\n\t\t},\n\n\t\tsuggestLabel() {\n\t\t\treturn t('nextcloud-vue', 'Suggest feature')\n\t\t},\n\n\t\tdisabledTitle() {\n\t\t\treturn t('nextcloud-vue', 'This feature has been disabled by your administrator')\n\t\t},\n\n\t\tdisabledDescription() {\n\t\t\treturn t('nextcloud-vue', 'Contact your Nextcloud administrator to enable Features & Roadmap on this instance.')\n\t\t},\n\n\t\tresolvedOpenbuiltUrl() {\n\t\t\tif (this.openbuiltUrl) {\n\t\t\t\treturn this.openbuiltUrl\n\t\t\t}\n\t\t\treturn generateUrl(DEFAULT_OPENBUILT_PATH)\n\t\t},\n\n\t\tresolvedLlmSkillsUrl() {\n\t\t\treturn this.llmSkillsUrl || DEFAULT_LLM_SKILLS_URL\n\t\t},\n\n\t\tresolvedDocumentationUrl() {\n\t\t\treturn this.documentationUrl || ''\n\t\t},\n\n\t\trepoSlug() {\n\t\t\tif (!this.repo || !this.repo.includes('/')) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.repo.split('/')[1].toLowerCase()\n\t\t},\n\n\t\tresolvedAppSlug() {\n\t\t\treturn this.appSlug || this.repoSlug\n\t\t},\n\n\t\tresolvedAppName() {\n\t\t\tif (this.appName) {\n\t\t\t\treturn this.appName\n\t\t\t}\n\t\t\tconst slug = this.repoSlug\n\t\t\tif (!slug) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn slug.charAt(0).toUpperCase() + slug.slice(1)\n\t\t},\n\n\t\tresolvedAppStoreUrl() {\n\t\t\treturn this.appStoreUrl || (this.resolvedAppSlug\n\t\t\t\t? `https://apps.nextcloud.com/apps/${this.resolvedAppSlug}`\n\t\t\t\t: '')\n\t\t},\n\n\t\tresolvedFeatureRequestUrl() {\n\t\t\tif (this.featureRequestUrl) {\n\t\t\t\treturn this.featureRequestUrl\n\t\t\t}\n\t\t\tif (!this.repo) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\t// The feature-request issue FORM, not the blank new-issue page —\n\t\t\t// the form's structured fields replaced the removed in-product\n\t\t\t// suggestion modal.\n\t\t\treturn buildFeatureRequestUrl(this.forge, this.repo)\n\t\t},\n\n\t\t/**\n\t\t * The Suggest CTA's target: the host's `suggestUrl` override when\n\t\t * given (a public form, a Discord channel), else the forge's\n\t\t * feature-request issue form.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedSuggestUrl() {\n\t\t\treturn this.suggestUrl || this.resolvedFeatureRequestUrl\n\t\t},\n\n\t\tdocumentationUrlIsExternal() {\n\t\t\treturn /^https?:\\/\\//i.test(this.resolvedDocumentationUrl)\n\t\t},\n\n\t\tdocsNoteLeading() {\n\t\t\treturn t('nextcloud-vue', 'Looking for documentation? Visit')\n\t\t},\n\n\t\tdocsNoteLinkLabel() {\n\t\t\t// Strip protocol for a cleaner inline link label.\n\t\t\treturn this.resolvedDocumentationUrl.replace(/^https?:\\/\\//i, '')\n\t\t},\n\n\t\tdocsNoteTrailing() {\n\t\t\treturn t('nextcloud-vue', 'for all technical and user documentation.')\n\t\t},\n\t},\n\n\twatch: {\n\t\t// A comparison that goes away leaves the reader on a stop that no\n\t\t// longer exists: the panel's v-else-if still matches, so it renders an\n\t\t// empty CnCapabilityTable under a \"Capabilities\" heading until the\n\t\t// reader thinks to press the toggle.\n\t\tavailableViews(views) {\n\t\t\tif (!views.includes(this.activeView)) {\n\t\t\t\tthis.activeView = views[0]\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.publishHoistedSidebar()\n\t\tif (this.capabilityComparison) {\n\t\t\twarnCapabilityComparisonDeprecated('CnFeaturesAndRoadmapView `capabilityComparison`')\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\t// Clear the holder so the hoisted sidebar disappears when the\n\t\t// user navigates away from this route. Mirrors CnIndexPage.\n\t\tif (this.cnHostsIndexSidebar && this.cnIndexSidebarConfig) {\n\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * @param {string} view One of the `availableViews`.\n\t\t * @return {string} The heading for that view.\n\t\t */\n\t\tviewTitle(view) {\n\t\t\tif (view === 'capabilities') {\n\t\t\t\treturn t('nextcloud-vue', 'Capabilities')\n\t\t\t}\n\t\t\treturn view === 'roadmap'\n\t\t\t\t? t('nextcloud-vue', 'Roadmap')\n\t\t\t\t: t('nextcloud-vue', 'Features')\n\t\t},\n\n\t\ttoggleView() {\n\t\t\tthis.activeView = this.nextView\n\t\t},\n\n\t\topenSupportDialog() {\n\t\t\tthis.showSupportDialog = true\n\t\t},\n\n\t\t/**\n\t\t * Publish the hoisted-sidebar config to `cnIndexSidebarConfig`\n\t\t * (same holder CnIndexPage uses) so CnAppRoot mounts the\n\t\t * CnFeaturesAndRoadmapSidebar at NcContent level. The sidebar\n\t\t * emits `suggest` when the user clicks the Suggest CTA inside\n\t\t * it; the view forwards that to `openSuggestModal()`. No-op\n\t\t * when there's no CnAppRoot ancestor.\n\t\t */\n\t\tpublishHoistedSidebar() {\n\t\t\tif (!this.cnHostsIndexSidebar || !this.cnIndexSidebarConfig) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The board look has no right-hand column: its links sit in the\n\t\t\t// board's Actions menu.\n\t\t\tif (this.disabled || this.isBoardLook) {\n\t\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.cnIndexSidebarConfig.value = {\n\t\t\t\t// markRaw: see CnIndexPage's publishHoistedSidebar — the holder\n\t\t\t\t// is deeply reactive and would proxy the component definition.\n\t\t\t\tcomponent: markRaw(CnFeaturesAndRoadmapSidebar),\n\t\t\t\tprops: {\n\t\t\t\t\topenbuiltUrl: this.resolvedOpenbuiltUrl,\n\t\t\t\t\tllmSkillsUrl: this.resolvedLlmSkillsUrl,\n\t\t\t\t\t// Always a URL now (the removed in-product modal was the\n\t\t\t\t\t// only non-link path), so the sidebar CTA always renders\n\t\t\t\t\t// as an anchor and its `suggest` emit path stays idle.\n\t\t\t\t\tsuggestUrl: this.resolvedSuggestUrl,\n\t\t\t\t},\n\n\t\t\t\tlisteners: {\n\t\t\t\t\tsupport: () => this.openSupportDialog(),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-features-and-roadmap-view {\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n\tpadding: 24px 16px;\n}\n\n.cn-features-and-roadmap-view__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin-bottom: 24px;\n\tflex-wrap: wrap;\n\t/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n\t positioned at the left edge of .app-content) plus 12px breathing\n\t room. Only the HEADER shifts right — the panel below keeps full width. */\n\tpadding-inline-start: 56px;\n}\n\n.cn-features-and-roadmap-view__title {\n\tfont-size: 1.4em;\n\tmargin: 0;\n\tcolor: var(--color-main-text);\n}\n\n.cn-features-and-roadmap-view__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\talign-items: center;\n\tflex-wrap: wrap;\n}\n\n.cn-features-and-roadmap-view__panel {\n\tmin-height: 320px;\n}\n\n/* Underline the inline docs URL so it reads as a link inside the note\n body — NcNoteCard's default link styling drops the underline by\n inheriting from the host theme. */\n.cn-features-and-roadmap-view__docs-note :deep(a) {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-features-and-roadmap-view__docs-note :deep(a:hover) {\n\ttext-decoration: underline;\n\topacity: 0.85;\n}\n</style>\n"],"names":["CnCapabilityTable","CnFeaturesBoard","CnFeaturesTab","CnRoadmapTab","CnSupportDialog","t","CnFeaturesAndRoadmapSidebar"],"mappings":"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"}
1
+ {"version":3,"file":"CnFeaturesAndRoadmapView.vue2.js","sources":["../../../../src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-features-and-roadmap-view\">\n\t\t<div v-if=\"disabled\" class=\"cn-features-and-roadmap-view__disabled\">\n\t\t\t<NcEmptyContent :name=\"disabledTitle\" :description=\"disabledDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LockOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<CnFeaturesBoard\n\t\t\tv-else-if=\"isBoardLook\"\n\t\t\t:features=\"features\"\n\t\t\t:categories=\"categories\"\n\t\t\t:repo=\"repo\"\n\t\t\t:title=\"title\"\n\t\t\t:appVersion=\"appVersion\"\n\t\t\t:documentationUrl=\"resolvedDocumentationUrl\"\n\t\t\t:suggestUrl=\"resolvedSuggestUrl\"\n\t\t\t:openbuiltUrl=\"resolvedOpenbuiltUrl\"\n\t\t\t:llmSkillsUrl=\"resolvedLlmSkillsUrl\"\n\t\t\tv-bind=\"boardOptions\"\n\t\t\t@support=\"openSupportDialog\" />\n\t\t<template v-else>\n\t\t\t<header class=\"cn-features-and-roadmap-view__header\">\n\t\t\t\t<h2 class=\"cn-features-and-roadmap-view__title\">\n\t\t\t\t\t{{ headerTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-features-and-roadmap-view__actions\">\n\t\t\t\t\t<NcButton @click=\"toggleView\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<TableSearch v-if=\"nextView === 'capabilities'\" :size=\"20\" />\n\t\t\t\t\t\t\t<RoadVariant v-else-if=\"nextView === 'roadmap'\" :size=\"20\" />\n\t\t\t\t\t\t\t<FormatListBulleted v-else :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ toggleLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<!-- Direct link to the forge's feature-request issue form\n\t\t\t\t\t (team decision 2026-09-04: the in-product suggestion\n\t\t\t\t\t modal is gone — the forge is where the conversation\n\t\t\t\t\t happens, in English). -->\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"resolvedSuggestUrl\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t:href=\"resolvedSuggestUrl\"\n\t\t\t\t\t\ttarget=\"_blank\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ suggestLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t<NcNoteCard\n\t\t\t\tv-if=\"resolvedDocumentationUrl\"\n\t\t\t\ttype=\"info\"\n\t\t\t\tclass=\"cn-features-and-roadmap-view__docs-note\">\n\t\t\t\t{{ docsNoteLeading }}\n\t\t\t\t<a\n\t\t\t\t\t:href=\"resolvedDocumentationUrl\"\n\t\t\t\t\t:target=\"documentationUrlIsExternal ? '_blank' : null\"\n\t\t\t\t\t:rel=\"documentationUrlIsExternal ? 'noopener noreferrer' : null\">{{ docsNoteLinkLabel }}</a>\n\t\t\t\t{{ docsNoteTrailing }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<main class=\"cn-features-and-roadmap-view__panel\">\n\t\t\t\t<CnFeaturesTab v-if=\"activeView === 'features'\" :features=\"features\" />\n\t\t\t\t<CnCapabilityTable\n\t\t\t\t\tv-else-if=\"activeView === 'capabilities'\"\n\t\t\t\t\t:comparison=\"capabilityComparison\" />\n\t\t\t\t<CnRoadmapTab v-else :repo=\"repo\" />\n\t\t\t</main>\n\t\t</template>\n\t\t<CnSupportDialog\n\t\t\tv-if=\"showSupportDialog && !disabled\"\n\t\t\t:appName=\"resolvedAppName\"\n\t\t\t:appSlug=\"resolvedAppSlug\"\n\t\t\t:appStoreUrl=\"resolvedAppStoreUrl\"\n\t\t\t:featureRequestUrl=\"resolvedFeatureRequestUrl\"\n\t\t\t:donateUrl=\"donateUrl\"\n\t\t\t:supportUrl=\"supportUrl\"\n\t\t\t:founderName=\"founderName\"\n\t\t\t:founderTitle=\"founderTitle\"\n\t\t\t@close=\"showSupportDialog = false\" />\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnFeaturesAndRoadmapView — route-level container for the Features &\n * Roadmap surface. Header carries the view-toggle and the primary\n * Suggest-feature CTA. Body is a card grid (Features OR Roadmap).\n *\n * The right-edge sidebar is hoisted to NcContent level via the same\n * `cnIndexSidebarConfig` provide mechanism CnIndexPage uses for its\n * CnIndexSidebar — that's the only place where Nextcloud's right-edge\n * sidebar slot renders correctly (next to NcAppContent, not inside it).\n * When mounted under CnAppRoot the view publishes\n * `CnFeaturesAndRoadmapSidebar` + props into the holder on mounted()\n * and clears it on beforeUnmount(). Sidebar carries four pitch\n * sections: Suggest, OpenBuilt, LLM, Support. Suggest renders as an\n * anchor to the forge's feature-request issue form (`resolvedSuggestUrl`\n * is always passed); Support emits `@support` (forwarded to a\n * freshly-mounted `CnSupportDialog`).\n *\n * Sidebar link targets are overridable via the `openbuiltUrl` and\n * `llmSkillsUrl` props. Defaults: in-instance `/apps/openbuilt` (via\n * `generateUrl`) and `https://docs.conduction.nl/ai-skills`.\n *\n * Spec: features-roadmap-component — Requirement \"CnFeaturesAndRoadmapView\".\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcEmptyContent, NcNoteCard } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport FormatListBulleted from 'vue-material-design-icons/FormatListBulleted.vue'\nimport LockOutline from 'vue-material-design-icons/LockOutline.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport RoadVariant from 'vue-material-design-icons/RoadVariant.vue'\nimport TableSearch from 'vue-material-design-icons/TableSearch.vue'\nimport CnCapabilityTable from '../CnCapabilityTable/CnCapabilityTable.vue'\nimport CnFeaturesAndRoadmapSidebar from '../CnFeaturesAndRoadmapSidebar/CnFeaturesAndRoadmapSidebar.vue'\nimport CnFeaturesBoard from '../CnFeaturesBoard/CnFeaturesBoard.vue'\nimport CnFeaturesTab from '../CnFeaturesTab/CnFeaturesTab.vue'\nimport CnRoadmapTab from '../CnRoadmapTab/CnRoadmapTab.vue'\nimport CnSupportDialog from '../CnSupportDialog/CnSupportDialog.vue'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { warnCapabilityComparisonDeprecated } from '../../utils/capabilityComparison.js'\nimport { buildFeatureRequestUrl, DEFAULT_FORGE } from '../../utils/forge.js'\n\nconst DEFAULT_OPENBUILT_PATH = '/apps/openbuilt'\nconst DEFAULT_LLM_SKILLS_URL = 'https://docs.conduction.nl/ai-skills'\nconst DEFAULT_DONATE_URL = 'https://github.com/sponsors/ConductionNL'\nconst DEFAULT_SUPPORT_URL = 'https://www.conduction.nl/support'\nconst DEFAULT_FOUNDER_NAME = 'Ruben van der Linde'\nconst DEFAULT_FOUNDER_TITLE = 'a founder of Conduction'\n\nexport default {\n\tname: 'CnFeaturesAndRoadmapView',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcNoteCard,\n\t\tFormatListBulleted,\n\t\tLockOutline,\n\t\tPlus,\n\t\tRoadVariant,\n\t\tTableSearch,\n\t\tCnCapabilityTable,\n\t\tCnFeaturesBoard,\n\t\tCnFeaturesTab,\n\t\tCnRoadmapTab,\n\t\tCnSupportDialog,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Set to `true` by CnAppRoot so descendants know the host will\n\t\t * mount a hoisted sidebar at NcContent level (see CnAppRoot.vue\n\t\t * `cnHostsIndexSidebar: true`). When `false` (no CnAppRoot\n\t\t * ancestor) the view skips publishing — there's no inline\n\t\t * fallback for this surface.\n\t\t */\n\t\tcnHostsIndexSidebar: { default: false },\n\t\t/**\n\t\t * Reactive holder published by CnAppRoot that backs the\n\t\t * hoisted-sidebar render in NcContent. Set value to\n\t\t * `{component, props, listeners}` to mount; set to `null` to\n\t\t * unmount. Same holder CnIndexPage uses.\n\t\t */\n\t\tcnIndexSidebarConfig: { default: () => ({ value: null }) },\n\t\t/** The app's look, provided by CnAppRoot (`nextcloud` or `board`). */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * `<owner>/<repo>` of the app's repository on the forge.\n\t\t */\n\t\trepo: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Target forge the feature-request deep-links are built against\n\t\t * (the Suggest CTA and the support dialog's feature-request URL).\n\t\t * Set `{type: 'github'}` (optionally with `baseUrl`) to switch\n\t\t * forge.\n\t\t *\n\t\t * @type {{type: 'codeberg'|'forgejo'|'gitea'|'github', baseUrl?: string}}\n\t\t */\n\t\tforge: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({ ...DEFAULT_FORGE }),\n\t\t},\n\n\t\t/**\n\t\t * Build-time feature manifest (alphabetical list rendered by CnFeaturesTab).\n\t\t *\n\t\t * @type {Array<{slug: string, title: string, summary: string, docsUrl: string}>}\n\t\t */\n\t\tfeatures: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Feature category key to caption, in the order the board look shows\n\t\t * the groups. A feature names its group with `category`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Board look options, passed to CnFeaturesBoard: `layout` (`cards` or\n\t\t * `rows`), `tabs`, `filter` (`chips` or `segmented`), `statusLabels`,\n\t\t * `next`, `issueLabels`, `subtitle`, `notice`, `noticeLink`,\n\t\t * `summaries`, `documentationButton` and `suggestButton`. Ignored in the default look.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tboard: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Page title in the board look. Empty reads \"Features and roadmap\".\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The app version, for `{appVersion}` in the board look's subtitle.\n\t\t */\n\t\tappVersion: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Admin opt-out flag — when true the entire view collapses to a\n\t\t * single \"disabled by admin\" empty state.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional capability comparison. When supplied, the header toggle\n\t\t * gains a third stop that renders `CnCapabilityTable`: a searchable\n\t\t * table of the rows, grouped by feature or by area, with a\n\t\t * provided-by column naming which app or platform delivers each one.\n\t\t *\n\t\t * Shape: `{systems, areas, features?, providers?, capabilities}`. The\n\t\t * rows carry the competitor ratings they already carry, plus the\n\t\t * optional `provider`, `providerHow`, `feature` and\n\t\t * `featureConfidence` fields.\n\t\t *\n\t\t * When null (default) the toggle keeps its two stops and the view\n\t\t * renders exactly as it does for every app that passes nothing.\n\t\t *\n\t\t * Deprecated: publish the comparison on the app's docs site and link to it (as dossiq #3312 and pipelinq #2195 did). Still rendered; removal waits for a major version.\n\t\t * A development build warns once when it is set.\n\t\t *\n\t\t * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated\n\t\t * @type {object|null}\n\t\t */\n\t\tcapabilityComparison: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Override the OpenBuilt sidebar CTA target. Pass an absolute URL or\n\t\t * a Nextcloud-relative path. When unset, defaults to the in-instance\n\t\t * OpenBuilt route at `/apps/openbuilt` (resolved via `generateUrl`).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\topenbuiltUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Override the LLM-skills sidebar CTA target. Defaults to\n\t\t * `https://docs.conduction.nl/ai-skills`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tllmSkillsUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional override for the Suggest CTA inside the sidebar. When\n\t\t * set the CTA renders as an anchor pointing at this URL —\n\t\t * appropriate when the app routes feature suggestions through a\n\t\t * public form, a Discord channel, or any non-forge target. When\n\t\t * empty (default) the CTA links the forge's feature-request\n\t\t * issue form derived from `repo` + `forge`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsuggestUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional URL of the app's public documentation site. When set,\n\t\t * an info banner is rendered above the card grid pointing users at\n\t\t * `<docs>` for full technical + user docs. Per-app — pipelinq\n\t\t * passes `https://pipelinq.conduction.nl`, decidesk passes its\n\t\t * own, etc. When empty (default) no banner renders.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Display name of the host app, used as the title of the bundled\n\t\t * `CnSupportDialog` and interpolated into its body copy. When\n\t\t * empty (default) the View derives a humanised label from `repo`\n\t\t * — `ConductionNL/openregister` becomes `Openregister` — which is\n\t\t * fine for stock apps and overridable for camelCased names.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Kebab-case host-app id passed to `CnSupportDialog` (its\n\t\t * localStorage namespace). When empty (default) the View derives\n\t\t * it from `repo` — second segment lower-cased.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappSlug: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Nextcloud App Store listing URL for the host app, threaded\n\t\t * through to `CnSupportDialog`. When empty (default) the View\n\t\t * derives `https://apps.nextcloud.com/apps/{appSlug}`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappStoreUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * URL the \"Suggest a feature\" CTA inside `CnSupportDialog` opens.\n\t\t * When empty (default) the View derives\n\t\t * `<forge-base>/{repo}/issues/new` from the `forge` prop. This is\n\t\t * intentionally separate from the prop-of-the-same-name on the\n\t\t * sidebar's Suggest CTA — that one opens the modal-driven flow;\n\t\t * this one is the \"open on the forge\" fallback used by the support\n\t\t * dialog, which is meant for the casual visitor rather than the\n\t\t * existing suggest-feature loop.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tfeatureRequestUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Donate-CTA URL passed to `CnSupportDialog`. Defaults to\n\t\t * ConductionNL's GitHub Sponsors page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdonateUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_DONATE_URL,\n\t\t},\n\n\t\t/**\n\t\t * Business-support CTA URL passed to `CnSupportDialog`. Defaults\n\t\t * to the Conduction contact page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tsupportUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_SUPPORT_URL,\n\t\t},\n\n\t\t/**\n\t\t * Name rendered in the handwritten signature of `CnSupportDialog`.\n\t\t * Defaults to Ruben van der Linde.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tfounderName: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_FOUNDER_NAME,\n\t\t},\n\n\t\t/**\n\t\t * Title shown after the handwritten signature in `CnSupportDialog`\n\t\t * (e.g. \"Founder\", \"Oprichter\").\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tfounderTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_FOUNDER_TITLE,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveView: 'features',\n\t\t\tshowSupportDialog: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the page is drawn in the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * The board options CnFeaturesBoard declares, so an unknown key in the\n\t\t * manifest never lands on its root element as an attribute.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tboardOptions() {\n\t\t\tconst keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton', 'actionsMenu']\n\t\t\tconst options = {}\n\t\t\tfor (const key of keys) {\n\t\t\t\tif (this.board && this.board[key] !== undefined && this.board[key] !== null) {\n\t\t\t\t\toptions[key] = this.board[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The stops the header toggle cycles through. Without a capability\n\t\t * comparison this is the two-stop list every consumer has today, and\n\t\t * the toggle behaves exactly as it did.\n\t\t *\n\t\t * @return {Array<string>} The views, in cycle order.\n\t\t */\n\t\tavailableViews() {\n\t\t\treturn this.capabilityComparison\n\t\t\t\t? ['features', 'capabilities', 'roadmap']\n\t\t\t\t: ['features', 'roadmap']\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The view the toggle moves to next.\n\t\t */\n\t\tnextView() {\n\t\t\tconst views = this.availableViews\n\t\t\tconst index = views.indexOf(this.activeView)\n\t\t\treturn views[(index + 1) % views.length]\n\t\t},\n\n\t\theaderTitle() {\n\t\t\treturn this.viewTitle(this.activeView)\n\t\t},\n\n\t\ttoggleLabel() {\n\t\t\tif (this.nextView === 'capabilities') {\n\t\t\t\treturn t('nextcloud-vue', 'Show capabilities')\n\t\t\t}\n\t\t\treturn this.nextView === 'roadmap'\n\t\t\t\t? t('nextcloud-vue', 'Show roadmap')\n\t\t\t\t: t('nextcloud-vue', 'Show features')\n\t\t},\n\n\t\tsuggestLabel() {\n\t\t\treturn t('nextcloud-vue', 'Suggest feature')\n\t\t},\n\n\t\tdisabledTitle() {\n\t\t\treturn t('nextcloud-vue', 'This feature has been disabled by your administrator')\n\t\t},\n\n\t\tdisabledDescription() {\n\t\t\treturn t('nextcloud-vue', 'Contact your Nextcloud administrator to enable Features & Roadmap on this instance.')\n\t\t},\n\n\t\tresolvedOpenbuiltUrl() {\n\t\t\tif (this.openbuiltUrl) {\n\t\t\t\treturn this.openbuiltUrl\n\t\t\t}\n\t\t\treturn generateUrl(DEFAULT_OPENBUILT_PATH)\n\t\t},\n\n\t\tresolvedLlmSkillsUrl() {\n\t\t\treturn this.llmSkillsUrl || DEFAULT_LLM_SKILLS_URL\n\t\t},\n\n\t\tresolvedDocumentationUrl() {\n\t\t\treturn this.documentationUrl || ''\n\t\t},\n\n\t\trepoSlug() {\n\t\t\tif (!this.repo || !this.repo.includes('/')) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.repo.split('/')[1].toLowerCase()\n\t\t},\n\n\t\tresolvedAppSlug() {\n\t\t\treturn this.appSlug || this.repoSlug\n\t\t},\n\n\t\tresolvedAppName() {\n\t\t\tif (this.appName) {\n\t\t\t\treturn this.appName\n\t\t\t}\n\t\t\tconst slug = this.repoSlug\n\t\t\tif (!slug) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn slug.charAt(0).toUpperCase() + slug.slice(1)\n\t\t},\n\n\t\tresolvedAppStoreUrl() {\n\t\t\treturn this.appStoreUrl || (this.resolvedAppSlug\n\t\t\t\t? `https://apps.nextcloud.com/apps/${this.resolvedAppSlug}`\n\t\t\t\t: '')\n\t\t},\n\n\t\tresolvedFeatureRequestUrl() {\n\t\t\tif (this.featureRequestUrl) {\n\t\t\t\treturn this.featureRequestUrl\n\t\t\t}\n\t\t\tif (!this.repo) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\t// The feature-request issue FORM, not the blank new-issue page —\n\t\t\t// the form's structured fields replaced the removed in-product\n\t\t\t// suggestion modal.\n\t\t\treturn buildFeatureRequestUrl(this.forge, this.repo)\n\t\t},\n\n\t\t/**\n\t\t * The Suggest CTA's target: the host's `suggestUrl` override when\n\t\t * given (a public form, a Discord channel), else the forge's\n\t\t * feature-request issue form.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedSuggestUrl() {\n\t\t\treturn this.suggestUrl || this.resolvedFeatureRequestUrl\n\t\t},\n\n\t\tdocumentationUrlIsExternal() {\n\t\t\treturn /^https?:\\/\\//i.test(this.resolvedDocumentationUrl)\n\t\t},\n\n\t\tdocsNoteLeading() {\n\t\t\treturn t('nextcloud-vue', 'Looking for documentation? Visit')\n\t\t},\n\n\t\tdocsNoteLinkLabel() {\n\t\t\t// Strip protocol for a cleaner inline link label.\n\t\t\treturn this.resolvedDocumentationUrl.replace(/^https?:\\/\\//i, '')\n\t\t},\n\n\t\tdocsNoteTrailing() {\n\t\t\treturn t('nextcloud-vue', 'for all technical and user documentation.')\n\t\t},\n\t},\n\n\twatch: {\n\t\t// A comparison that goes away leaves the reader on a stop that no\n\t\t// longer exists: the panel's v-else-if still matches, so it renders an\n\t\t// empty CnCapabilityTable under a \"Capabilities\" heading until the\n\t\t// reader thinks to press the toggle.\n\t\tavailableViews(views) {\n\t\t\tif (!views.includes(this.activeView)) {\n\t\t\t\tthis.activeView = views[0]\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.publishHoistedSidebar()\n\t\tif (this.capabilityComparison) {\n\t\t\twarnCapabilityComparisonDeprecated('CnFeaturesAndRoadmapView `capabilityComparison`')\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\t// Clear the holder so the hoisted sidebar disappears when the\n\t\t// user navigates away from this route. Mirrors CnIndexPage.\n\t\tif (this.cnHostsIndexSidebar && this.cnIndexSidebarConfig) {\n\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * @param {string} view One of the `availableViews`.\n\t\t * @return {string} The heading for that view.\n\t\t */\n\t\tviewTitle(view) {\n\t\t\tif (view === 'capabilities') {\n\t\t\t\treturn t('nextcloud-vue', 'Capabilities')\n\t\t\t}\n\t\t\treturn view === 'roadmap'\n\t\t\t\t? t('nextcloud-vue', 'Roadmap')\n\t\t\t\t: t('nextcloud-vue', 'Features')\n\t\t},\n\n\t\ttoggleView() {\n\t\t\tthis.activeView = this.nextView\n\t\t},\n\n\t\topenSupportDialog() {\n\t\t\tthis.showSupportDialog = true\n\t\t},\n\n\t\t/**\n\t\t * Publish the hoisted-sidebar config to `cnIndexSidebarConfig`\n\t\t * (same holder CnIndexPage uses) so CnAppRoot mounts the\n\t\t * CnFeaturesAndRoadmapSidebar at NcContent level. The sidebar\n\t\t * emits `suggest` when the user clicks the Suggest CTA inside\n\t\t * it; the view forwards that to `openSuggestModal()`. No-op\n\t\t * when there's no CnAppRoot ancestor.\n\t\t */\n\t\tpublishHoistedSidebar() {\n\t\t\tif (!this.cnHostsIndexSidebar || !this.cnIndexSidebarConfig) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The board look has no right-hand column: its links sit in the\n\t\t\t// board's Actions menu.\n\t\t\tif (this.disabled || this.isBoardLook) {\n\t\t\t\tthis.cnIndexSidebarConfig.value = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.cnIndexSidebarConfig.value = {\n\t\t\t\t// markRaw: see CnIndexPage's publishHoistedSidebar — the holder\n\t\t\t\t// is deeply reactive and would proxy the component definition.\n\t\t\t\tcomponent: markRaw(CnFeaturesAndRoadmapSidebar),\n\t\t\t\tprops: {\n\t\t\t\t\topenbuiltUrl: this.resolvedOpenbuiltUrl,\n\t\t\t\t\tllmSkillsUrl: this.resolvedLlmSkillsUrl,\n\t\t\t\t\t// Always a URL now (the removed in-product modal was the\n\t\t\t\t\t// only non-link path), so the sidebar CTA always renders\n\t\t\t\t\t// as an anchor and its `suggest` emit path stays idle.\n\t\t\t\t\tsuggestUrl: this.resolvedSuggestUrl,\n\t\t\t\t},\n\n\t\t\t\tlisteners: {\n\t\t\t\t\tsupport: () => this.openSupportDialog(),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-features-and-roadmap-view {\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n\tpadding: 24px 16px;\n}\n\n.cn-features-and-roadmap-view__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin-bottom: 24px;\n\tflex-wrap: wrap;\n\t/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n\t positioned at the left edge of .app-content) plus 12px breathing\n\t room. Only the HEADER shifts right — the panel below keeps full width. */\n\tpadding-inline-start: 56px;\n}\n\n.cn-features-and-roadmap-view__title {\n\tfont-size: 1.4em;\n\tmargin: 0;\n\tcolor: var(--color-main-text);\n}\n\n.cn-features-and-roadmap-view__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\talign-items: center;\n\tflex-wrap: wrap;\n}\n\n.cn-features-and-roadmap-view__panel {\n\tmin-height: 320px;\n}\n\n/* Underline the inline docs URL so it reads as a link inside the note\n body — NcNoteCard's default link styling drops the underline by\n inheriting from the host theme. */\n.cn-features-and-roadmap-view__docs-note :deep(a) {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-features-and-roadmap-view__docs-note :deep(a:hover) {\n\ttext-decoration: underline;\n\topacity: 0.85;\n}\n</style>\n"],"names":["CnCapabilityTable","CnFeaturesBoard","CnFeaturesTab","CnRoadmapTab","CnSupportDialog","t","CnFeaturesAndRoadmapSidebar"],"mappings":"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"}