@conduction/nextcloud-vue 2.78.0 → 2.80.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 (259) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
  9. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
  19. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  40. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  42. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  43. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  44. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  46. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  50. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  51. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  52. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  54. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  56. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  57. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  58. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
  60. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  62. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  64. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  71. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  72. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  73. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  74. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  75. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  81. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  82. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  83. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  84. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  85. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  92. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  93. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  95. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  96. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  98. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  99. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  100. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  101. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  102. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  103. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  104. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  105. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  106. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  110. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
  112. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  113. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  114. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  115. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  116. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  117. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  118. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  119. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  120. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  121. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  122. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  123. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  124. package/dist/esm/composables/useAppManifest.js +8 -3
  125. package/dist/esm/composables/useAppManifest.js.map +1 -1
  126. package/dist/esm/composables/useFormDraft.js +140 -9
  127. package/dist/esm/composables/useFormDraft.js.map +1 -1
  128. package/dist/esm/index.js +5 -0
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  131. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  132. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  133. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  134. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  135. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  136. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  137. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  138. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  139. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  140. package/dist/esm/integrations/offline/index.js +2 -1
  141. package/dist/esm/integrations/offline/index.js.map +1 -1
  142. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  143. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  144. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  145. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  146. package/dist/esm/l10n/en.json.js +72 -1
  147. package/dist/esm/l10n/en.json.js.map +1 -1
  148. package/dist/esm/l10n/nl.json.js +120 -2
  149. package/dist/esm/l10n/nl.json.js.map +1 -1
  150. package/dist/esm/nextcloud-vue.css +1314 -74
  151. package/dist/esm/utils/boardDate.js +193 -0
  152. package/dist/esm/utils/boardDate.js.map +1 -0
  153. package/dist/esm/utils/detailActionModel.js +60 -3
  154. package/dist/esm/utils/detailActionModel.js.map +1 -1
  155. package/dist/esm/utils/dynamicProperties.js +5 -1
  156. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  157. package/dist/esm/utils/fillTemplate.js +114 -0
  158. package/dist/esm/utils/fillTemplate.js.map +1 -0
  159. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  160. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  161. package/dist/esm/utils/queryPresets.js +96 -0
  162. package/dist/esm/utils/queryPresets.js.map +1 -0
  163. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  164. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  165. package/dist/esm/utils/schema.js +21 -0
  166. package/dist/esm/utils/schema.js.map +1 -1
  167. package/dist/esm/utils/sentinelTokens.js +3 -3
  168. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  169. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  170. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  171. package/dist/esm/utils/widgetDispatch.js +7 -1
  172. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  173. package/dist/nextcloud-vue.cjs.js +14278 -8169
  174. package/dist/nextcloud-vue.cjs.js.map +2 -2
  175. package/dist/nextcloud-vue.css +1314 -74
  176. package/l10n/en.json +72 -1
  177. package/l10n/nl.json +120 -2
  178. package/package.json +1 -1
  179. package/scripts/build-validators.js +26 -0
  180. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  181. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  182. package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
  183. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  184. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  185. package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
  186. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  187. package/src/components/CnDataTable/CnDataTable.md +2 -0
  188. package/src/components/CnDataTable/CnDataTable.vue +82 -2
  189. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  190. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  191. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
  192. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  193. package/src/components/CnFactsWidget/index.js +29 -0
  194. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  195. package/src/components/CnFieldListWidget/index.js +28 -0
  196. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  197. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  198. package/src/components/CnFilesBrowser/index.js +1 -0
  199. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  200. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  201. package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
  202. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  203. package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
  204. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  205. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  206. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  207. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  208. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  209. package/src/components/CnRecordListWidget/index.js +30 -0
  210. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  211. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  212. package/src/components/CnSectionsWidget/index.js +34 -0
  213. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  214. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  215. package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
  216. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  217. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  218. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  219. package/src/components/index.js +5 -1
  220. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  221. package/src/composables/useAppManifest.js +8 -3
  222. package/src/composables/useFormDraft.js +139 -8
  223. package/src/css/form-field.css +14 -0
  224. package/src/css/index-page.css +71 -0
  225. package/src/css/kpi-card.css +32 -0
  226. package/src/css/look-board-detail.css +87 -0
  227. package/src/css/look-board-index.css +143 -14
  228. package/src/css/look-board.css +114 -2
  229. package/src/css/table.css +56 -0
  230. package/src/index.js +8 -0
  231. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  232. package/src/integrations/builtin/field-inspection.js +21 -0
  233. package/src/integrations/offline/checklistSubmission.js +137 -0
  234. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  235. package/src/integrations/offline/index.js +3 -0
  236. package/src/integrations/offline/offlineDb.js +6 -1
  237. package/src/integrations/offline/syncReplayService.js +8 -2
  238. package/src/schemas/app-manifest-v2.schema.json +149 -5
  239. package/src/utils/boardDate.js +189 -0
  240. package/src/utils/detailActionModel.js +59 -2
  241. package/src/utils/dynamicProperties.js +5 -1
  242. package/src/utils/fillTemplate.js +110 -0
  243. package/src/utils/libraryWidgetKeys.js +4 -0
  244. package/src/utils/queryPresets.js +93 -0
  245. package/src/utils/resolveFilterTokens.js +11 -0
  246. package/src/utils/schema.js +21 -0
  247. package/src/utils/sentinelTokens.js +3 -3
  248. package/src/utils/validateManifestV2.compiled.js +57 -2
  249. package/src/utils/widgetDispatch.js +7 -1
  250. package/dist/esm/_virtual/equal.js +0 -4
  251. package/dist/esm/_virtual/equal.js.map +0 -1
  252. package/dist/esm/_virtual/ucs2length.js +0 -4
  253. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  254. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  255. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  256. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  257. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  258. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  259. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cn-index-page"
3
- :class="lookClass"
3
+ :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel }]"
4
4
  data-testid="cn-index-page"
5
5
  @keydown="onListKeydown">
6
6
  <!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
@@ -76,6 +76,20 @@
76
76
  <slot name="below-header" />
77
77
  </div>
78
78
 
79
+ <!-- KPI tiles above the list (`statRow`) and cards beside it
80
+ (`sidePanel`), as DqTeamwachtrij and PqContracten
81
+ (screens-index-stat-row). -->
82
+ <CnIndexPageWidgets
83
+ v-if="statRow && statRow.length > 0"
84
+ class="cn-index-page__stat-row"
85
+ variant="stat-row"
86
+ :widgets="statRow" />
87
+ <CnIndexPageWidgets
88
+ v-if="hasSidePanel"
89
+ class="cn-index-page__side-panel"
90
+ variant="side-panel"
91
+ :widgets="sidePanel" />
92
+
79
93
  <!-- Actions bar -->
80
94
  <CnActionsBar
81
95
  :layout="isBoardLook ? 'board' : 'nextcloud'"
@@ -97,6 +111,7 @@
97
111
  :viewMode="currentViewMode"
98
112
  :showViewToggle="showViewToggle"
99
113
  :availableViewModes="effectiveToggleModes"
114
+ :disabledViewModes="viewSwitchDisabledModes"
100
115
  :cardsLabel="cardsLabel"
101
116
  :tableLabel="tableLabel"
102
117
  :listLabel="listLabel"
@@ -111,7 +126,7 @@
111
126
  :sortValue="sortSelectValue"
112
127
  :showSearch="inlineSearch || isBoardLook"
113
128
  :searchValue="effectiveSearchValue"
114
- :searchPlaceholder="searchPlaceholder"
129
+ :searchPlaceholder="searchPlaceholder ? cnTranslate(searchPlaceholder) : searchPlaceholder"
115
130
  :showCountWithSearch="showCountWithSearch"
116
131
  :refreshing="effectiveRefreshing"
117
132
  :refreshDisabled="refreshDisabled"
@@ -565,6 +580,7 @@
565
580
  :includeColumns="includeColumns"
566
581
  :columnOverrides="columnOverrides"
567
582
  :rowClass="rowClass"
583
+ :rowTitle="rowTitle"
568
584
  :filterable="tableHeaderFilters"
569
585
  :activeFilters="effectiveActiveFilters"
570
586
  :filterRegister="typeof register === 'string' ? register : ''"
@@ -958,6 +974,7 @@ import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
958
974
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
959
975
  import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
960
976
  import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
977
+ import CnIndexPageWidgets from './CnIndexPageWidgets.vue'
961
978
  import { useContextMenu } from '../../composables/index.js'
962
979
  import { useLook } from '../../composables/useLook.js'
963
980
  import { copyKindsOf } from '../../composables/useObjectCopy.js'
@@ -1242,6 +1259,7 @@ export default {
1242
1259
  name: 'CnIndexPage',
1243
1260
 
1244
1261
  components: {
1262
+ CnIndexPageWidgets,
1245
1263
  CnBuildiqEditButton,
1246
1264
  CnFollowToggle,
1247
1265
  NcLoadingIcon,
@@ -1533,6 +1551,21 @@ export default {
1533
1551
  default: () => [],
1534
1552
  },
1535
1553
 
1554
+ /**
1555
+ * How the board look draws the table's title column (manifest
1556
+ * `config.rowTitle`): `link` (the default) underlines the title,
1557
+ * `plain` draws it as bold text without an underline over a 13px muted
1558
+ * secondary line. The row stays clickable. The Nextcloud look ignores
1559
+ * it. Maps to CnDataTable `rowTitle`.
1560
+ *
1561
+ * @spec openspec/changes/screens-table-rows-parity/specs/index-list-board-look/spec.md#requirement-a-row-title-can-be-plain-text
1562
+ */
1563
+ rowTitle: {
1564
+ type: String,
1565
+ default: 'link',
1566
+ validator: (value) => ['link', 'plain'].includes(value),
1567
+ },
1568
+
1536
1569
  /** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */
1537
1570
  icon: {
1538
1571
  type: String,
@@ -1571,7 +1604,10 @@ export default {
1571
1604
  * `$route.params` just like the `filter` prop. The first tab with
1572
1605
  * `default:true` (else index 0) is active on mount; changing tabs
1573
1606
  * re-fetches at page 1. Omit (or `null`) → no tab strip, behaviour
1574
- * unchanged.
1607
+ * unchanged. Under the board look a tab with `activeLabel` ("Pipeline:
1608
+ * Sales") adds a removable chip with that text to the Active line and
1609
+ * counts on the Filter badge while it is selected; removing the chip
1610
+ * goes back to the default tab.
1575
1611
  */
1576
1612
  quickFilters: {
1577
1613
  type: Array,
@@ -1942,6 +1978,22 @@ export default {
1942
1978
  default: null,
1943
1979
  },
1944
1980
 
1981
+ /**
1982
+ * Board look: the segments the view switch draws, e.g. `['table',
1983
+ * 'cards', 'board', 'map']` as the boards do. A listed mode the page
1984
+ * can open is an ordinary segment; one it cannot (no board columns, no
1985
+ * map configured) is drawn disabled, named "<mode>: not available on
1986
+ * this list". Fed from `pages[].config.viewSwitch`. Ignored without the
1987
+ * board look, and absent the switch is as before.
1988
+ *
1989
+ * @type {Array<'table' | 'cards' | 'board' | 'map'>}
1990
+ * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
1991
+ */
1992
+ viewSwitch: {
1993
+ type: Array,
1994
+ default: null,
1995
+ },
1996
+
1945
1997
  /**
1946
1998
  * The board's configuration, mirroring the manifest `config.board`
1947
1999
  * block: `{ statusField, cardFields, swimlaneField }`. The board is
@@ -2628,6 +2680,35 @@ export default {
2628
2680
  default: null,
2629
2681
  },
2630
2682
 
2683
+ /**
2684
+ * KPI tiles above the list (manifest `config.statRow`): dashboard
2685
+ * widget definitions `{ id, type, title?, content }`, usually `stat`,
2686
+ * drawn in an auto-fit row (each at least 200px) in the listed order.
2687
+ * Types resolve like dashboard widgets: the app registry, then the
2688
+ * dashboard catalog. Empty by default.
2689
+ *
2690
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-stat-row-above-the-list
2691
+ * @type {Array<object>}
2692
+ */
2693
+ statRow: {
2694
+ type: Array,
2695
+ default: () => [],
2696
+ },
2697
+
2698
+ /**
2699
+ * Cards beside the list (manifest `config.sidePanel`): dashboard
2700
+ * widget definitions `{ id, type, title?, content, headerLink? }`
2701
+ * drawn in a 300px column right of the toolbar and the list. Empty
2702
+ * by default; below 1024px the column moves above the list.
2703
+ *
2704
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-side-panel-beside-the-list
2705
+ * @type {Array<object>}
2706
+ */
2707
+ sidePanel: {
2708
+ type: Array,
2709
+ default: () => [],
2710
+ },
2711
+
2631
2712
  /**
2632
2713
  * Where a row carries the actions the server says this caller may run
2633
2714
  * on it: a dotted path, read off the rows the list already fetched, so
@@ -3307,6 +3388,16 @@ export default {
3307
3388
  },
3308
3389
 
3309
3390
  computed: {
3391
+ /**
3392
+ * Whether the page draws cards beside its list (`sidePanel`).
3393
+ *
3394
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-side-panel-beside-the-list
3395
+ * @return {boolean}
3396
+ */
3397
+ hasSidePanel() {
3398
+ return Array.isArray(this.sidePanel) && this.sidePanel.length > 0
3399
+ },
3400
+
3310
3401
  /**
3311
3402
  * The empty-state copy run through the host translate function
3312
3403
  * (the injected `cnTranslate`, identity by default). Only this
@@ -3679,6 +3770,21 @@ export default {
3679
3770
  return Object.keys(this.mapConfig || {}).length > 0
3680
3771
  },
3681
3772
 
3773
+ /**
3774
+ * Board look: the modes `viewSwitch` lists that this page cannot open,
3775
+ * drawn as disabled segments.
3776
+ *
3777
+ * @return {Array<string>} The disabled modes.
3778
+ * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
3779
+ */
3780
+ viewSwitchDisabledModes() {
3781
+ if (!this.isBoardLook || !Array.isArray(this.viewSwitch)) {
3782
+ return []
3783
+ }
3784
+ const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
3785
+ return this.viewSwitch.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
3786
+ },
3787
+
3682
3788
  /**
3683
3789
  * The base view-toggle segments (cards/table/list) passed to CnActionsBar.
3684
3790
  * Beta's explicit `viewModes` whitelist wins when set; otherwise the
@@ -5248,11 +5354,11 @@ export default {
5248
5354
  return []
5249
5355
  }
5250
5356
  const props = this.effectiveSchema?.properties || {}
5251
- const chips = []
5357
+ const chips = [...this.quickFilterActiveChips]
5252
5358
  for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
5253
5359
  const values = (Array.isArray(raw) ? raw : [raw])
5254
5360
  .filter((value) => value !== null && value !== undefined && value !== '')
5255
- .map((value) => String(value))
5361
+ .map((value) => this.activeFilterValueLabel(key, value))
5256
5362
  if (values.length === 0) {
5257
5363
  continue
5258
5364
  }
@@ -5262,6 +5368,87 @@ export default {
5262
5368
  return chips
5263
5369
  },
5264
5370
 
5371
+ /**
5372
+ * The Active line's chips for the selected quick filters: one per
5373
+ * selected tab that declares an `activeLabel` ("Pipeline: Sales"), run
5374
+ * through the app's translate. A tab without one (an "All" tab) adds no
5375
+ * chip. The chip carries the tab's index so removing it can deselect it.
5376
+ *
5377
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-a-quick-filter-with-an-active-label-joins-the-active-line
5378
+ * @return {Array<{key: string, label: string, quickFilterIndex: number}>}
5379
+ */
5380
+ quickFilterActiveChips() {
5381
+ const tabs = this.tabStripEntries || []
5382
+ if (tabs.length === 0) {
5383
+ return []
5384
+ }
5385
+ const indices = this.quickFilterMultiple
5386
+ ? (this.selectedQuickFilterIndices || [])
5387
+ : [this.activeQuickFilterIndex]
5388
+ return indices
5389
+ .filter((index) => typeof index === 'number' && tabs[index] && typeof tabs[index].activeLabel === 'string' && tabs[index].activeLabel !== '')
5390
+ .map((index) => ({ key: `quick-filter:${index}`, label: this.cnTranslate(tabs[index].activeLabel), quickFilterIndex: index }))
5391
+ },
5392
+
5393
+ /**
5394
+ * Filter keys over a reference property that the Active line must name,
5395
+ * and that no reference column resolves already: one spec each, with the
5396
+ * referenced register and schema off the page schema's `$ref`.
5397
+ *
5398
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-an-active-chip-names-the-value-not-its-id
5399
+ * @return {Array<{key: string, labelField: string, register: string, schema: string}>}
5400
+ */
5401
+ activeFilterRefSpecs() {
5402
+ if (!this.isBoardLook || !this.effectiveSchema) {
5403
+ return []
5404
+ }
5405
+ const keys = Object.keys(this.effectiveActiveFilters || {})
5406
+ if (keys.length === 0) {
5407
+ return []
5408
+ }
5409
+ const covered = new Set(this.refLabelSpecs.map((spec) => spec.key))
5410
+ const fields = fieldsFromSchema(this.effectiveSchema, { includeReadOnly: true })
5411
+ const specs = []
5412
+ for (const key of keys) {
5413
+ if (covered.has(key)) {
5414
+ continue
5415
+ }
5416
+ const field = fields.find((f) => f.key === key)
5417
+ const ref = field && field.reference
5418
+ if (!ref || ref.schema === undefined || ref.schema === null) {
5419
+ continue
5420
+ }
5421
+ specs.push({
5422
+ key,
5423
+ labelField: '',
5424
+ register: ref.register || (typeof this.register === 'string' ? this.register : ''),
5425
+ schema: String(ref.schema),
5426
+ })
5427
+ }
5428
+ return specs
5429
+ },
5430
+
5431
+ /**
5432
+ * The ids each active reference filter holds, per filter key, for the
5433
+ * label lookup of the Active line.
5434
+ *
5435
+ * @return {{[key: string]: string[]}} Distinct ids per filter key.
5436
+ */
5437
+ activeFilterRefIds() {
5438
+ const out = {}
5439
+ const keys = new Set([...this.refLabelSpecs, ...this.activeFilterRefSpecs].map((spec) => spec.key))
5440
+ for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
5441
+ if (!keys.has(key)) {
5442
+ continue
5443
+ }
5444
+ out[key] = (Array.isArray(raw) ? raw : [raw])
5445
+ .filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')
5446
+ .map((id) => String(id))
5447
+ .sort()
5448
+ }
5449
+ return out
5450
+ },
5451
+
5265
5452
  /**
5266
5453
  * The plural the board bulk band names: "With the selected cases".
5267
5454
  * The schema's `titlePlural` (or `plural`), else the lower-cased
@@ -5344,6 +5531,7 @@ export default {
5344
5531
  * filled in when one is known, else `description`.
5345
5532
  *
5346
5533
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
5534
+ * @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-the-count-reads-in-the-users-language
5347
5535
  * @return {string}
5348
5536
  */
5349
5537
  headerDescription() {
@@ -5351,9 +5539,16 @@ export default {
5351
5539
  // Board look: the count line is a template over the rows on this page
5352
5540
  // and all rows, "{shown} of {total}" unless the page says otherwise.
5353
5541
  if (this.isBoardLook && typeof total === 'number' && total >= 0) {
5354
- const template = this.countText || this.countSubtitle || '{shown} of {total}'
5542
+ const shown = this.effectiveObjects.length
5543
+ // The default count line is the library's own string, so it
5544
+ // reads "14 van 14" in Dutch; a page's own template goes
5545
+ // through the app's label lookup.
5546
+ if (!this.countText && !this.countSubtitle) {
5547
+ return t('nextcloud-vue', '{shown} of {total}', { shown, total })
5548
+ }
5549
+ const template = this.countText || this.countSubtitle
5355
5550
  return this.cnTranslate(template)
5356
- .replace('{shown}', String(this.effectiveObjects.length))
5551
+ .replace('{shown}', String(shown))
5357
5552
  .replace('{total}', String(total))
5358
5553
  }
5359
5554
  if (this.countSubtitle && typeof total === 'number' && total >= 0) {
@@ -5534,6 +5729,14 @@ export default {
5534
5729
  },
5535
5730
  },
5536
5731
 
5732
+ // The Active line names a referenced value, not its id.
5733
+ activeFilterRefIds: {
5734
+ deep: true,
5735
+ handler() {
5736
+ this.loadRefLabels()
5737
+ },
5738
+ },
5739
+
5537
5740
  countRequestKey: {
5538
5741
  immediate: true,
5539
5742
  /** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views */
@@ -5802,7 +6005,7 @@ export default {
5802
6005
  * @spec openspec/changes/index-ref-column-labels/tasks.md#task-2
5803
6006
  */
5804
6007
  async loadRefLabels() {
5805
- const specs = this.refLabelSpecs
6008
+ const specs = [...this.refLabelSpecs, ...this.activeFilterRefSpecs]
5806
6009
  if (specs.length === 0) {
5807
6010
  return
5808
6011
  }
@@ -5817,7 +6020,8 @@ export default {
5817
6020
  }
5818
6021
  await Promise.all(specs.map(async (spec) => {
5819
6022
  const known = this.refLabels[spec.key] || {}
5820
- const ids = (this.refLabelIds[spec.key] || []).filter((id) => !Object.hasOwn(known, id))
6023
+ const wanted = new Set([...(this.refLabelIds[spec.key] || []), ...(this.activeFilterRefIds[spec.key] || [])])
6024
+ const ids = [...wanted].filter((id) => !Object.hasOwn(known, id))
5821
6025
  if (ids.length === 0) {
5822
6026
  return
5823
6027
  }
@@ -6501,6 +6705,11 @@ export default {
6501
6705
  * @return {void}
6502
6706
  */
6503
6707
  onClearFilters() {
6708
+ // "Clear all" on the Active line also undoes the quick filters it
6709
+ // listed (a tab without an `activeLabel` is not a filter there).
6710
+ for (const chip of this.quickFilterActiveChips) {
6711
+ this.deselectQuickFilterChip(chip.quickFilterIndex)
6712
+ }
6504
6713
  // Clearing the view brings the person's own columns back.
6505
6714
  this.appliedViewColumns = null
6506
6715
  this.appliedSavedViewId = ''
@@ -6591,11 +6800,74 @@ export default {
6591
6800
  * @spec openspec/changes/screens-index-list-parity/specs/index-list-board-look/spec.md#requirement-the-toolbar-sits-on-the-ground-in-two-rows
6592
6801
  */
6593
6802
  onRemoveActiveFilter(chip) {
6803
+ if (chip && typeof chip.quickFilterIndex === 'number') {
6804
+ this.deselectQuickFilterChip(chip.quickFilterIndex)
6805
+ return
6806
+ }
6594
6807
  if (chip && typeof chip.key === 'string') {
6595
6808
  this.onFilterEvent({ key: chip.key, values: [] })
6596
6809
  }
6597
6810
  },
6598
6811
 
6812
+ /**
6813
+ * Undo a quick filter from the Active line. With several tabs selectable
6814
+ * the tab leaves the selection; with one, the page goes back to its
6815
+ * default tab when that tab names no active filter itself, and else to no
6816
+ * tab at all, so the chip does not come straight back.
6817
+ *
6818
+ * @param {number} index The tab index the chip stands for.
6819
+ * @return {void}
6820
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-a-quick-filter-with-an-active-label-joins-the-active-line
6821
+ */
6822
+ deselectQuickFilterChip(index) {
6823
+ if (this.quickFilterMultiple) {
6824
+ this.onQuickFilterMultiChange((this.selectedQuickFilterIndices || []).filter((i) => i !== index))
6825
+ return
6826
+ }
6827
+ if (this.activeQuickFilterIndex !== index) {
6828
+ return
6829
+ }
6830
+ const tabs = this.tabStripEntries || []
6831
+ let fallback = tabs.findIndex((tab) => tab && tab.default === true)
6832
+ if (fallback < 0 && tabs.length > 0) {
6833
+ fallback = 0
6834
+ }
6835
+ const fallbackNamed = fallback >= 0 && typeof tabs[fallback].activeLabel === 'string' && tabs[fallback].activeLabel !== ''
6836
+ this.onQuickFilterChange(fallback >= 0 && fallback !== index && !fallbackNamed ? fallback : null)
6837
+ },
6838
+
6839
+ /**
6840
+ * The text an Active chip shows for one filter value: the referenced
6841
+ * object's label, the facet bucket's label, the schema's `oneOf` title,
6842
+ * else the value itself.
6843
+ *
6844
+ * @param {string} key The filter key.
6845
+ * @param {unknown} value One value of that filter.
6846
+ * @return {string} The label.
6847
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-an-active-chip-names-the-value-not-its-id
6848
+ */
6849
+ activeFilterValueLabel(key, value) {
6850
+ const id = String(value)
6851
+ const refs = this.refLabels[key]
6852
+ if (refs && typeof refs[id] === 'string' && refs[id] !== '') {
6853
+ return refs[id]
6854
+ }
6855
+ const facet = this.effectiveFacetData && this.effectiveFacetData[key]
6856
+ const bucket = facet && Array.isArray(facet.values)
6857
+ ? facet.values.find((fv) => fv && typeof fv === 'object' && String(fv.value) === id)
6858
+ : null
6859
+ if (bucket && typeof bucket.label === 'string' && bucket.label !== '') {
6860
+ return this.cnTranslate(bucket.label)
6861
+ }
6862
+ const prop = this.effectiveSchema?.properties?.[key]
6863
+ const options = prop && Array.isArray(prop.oneOf) ? prop.oneOf : []
6864
+ const option = options.find((o) => o && String(o.const) === id && typeof o.title === 'string')
6865
+ if (option) {
6866
+ return this.cnTranslate(option.title)
6867
+ }
6868
+ return id
6869
+ },
6870
+
6599
6871
  /**
6600
6872
  * A header filter was applied or cleared. One column can own several
6601
6873
  * query parameters (a range is `key[gte]` and `key[lte]`), so they are
@@ -0,0 +1,169 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+
5
+ CnIndexPageWidgets — the widgets an index page draws around its list
6
+ (screens-index-stat-row): the KPI tiles above it (`statRow`, an auto-fit row)
7
+ and the cards beside it (`sidePanel`, a column). Each entry is a dashboard
8
+ widget definition `{ id, type, title?, content, headerLink? }`, resolved like
9
+ a dashboard widget: the app's registry, then the dashboard catalog, then the
10
+ built-in widgets. Internal to CnIndexPage.
11
+ -->
12
+ <template>
13
+ <component
14
+ :is="variant === 'stat-row' ? 'CnKpiGrid' : 'aside'"
15
+ v-if="entries.length > 0"
16
+ :columns="variant === 'stat-row' ? 'auto' : undefined"
17
+ class="cn-index-page-widgets"
18
+ :class="['cn-index-page-widgets--' + variant]"
19
+ :data-testid="'cn-index-page-' + variant">
20
+ <div
21
+ v-for="entry in entries"
22
+ :key="entry.id"
23
+ class="cn-index-page-widgets__cell"
24
+ :data-testid="'cn-index-page-widget-' + entry.id">
25
+ <CnWidgetWrapper
26
+ :widgetId="entry.id"
27
+ :title="entry.title"
28
+ :showTitle="variant === 'side-panel' && entry.title !== ''"
29
+ :showActions="false"
30
+ :showRefresh="false"
31
+ :headerLink="entry.headerLink"
32
+ :flush="variant === 'stat-row'">
33
+ <component
34
+ :is="entry.renderer"
35
+ :widgetId="entry.id"
36
+ :content="entry.content"
37
+ v-bind="entry.content" />
38
+ </CnWidgetWrapper>
39
+ </div>
40
+ </component>
41
+ </template>
42
+
43
+ <script>
44
+ import CnKpiGrid from '../CnKpiGrid/CnKpiGrid.vue'
45
+ import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
46
+ import { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'
47
+ import { BUILT_IN_WIDGETS } from '../CnWidgetGrid/builtInWidgets.js'
48
+ import { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
49
+
50
+ // Populate the dashboard catalog (stat, table, people, …): an index page
51
+ // may be the first surface that resolves a widget type.
52
+ import '../CnWidgetGrid/registerDashboardWidgets.js'
53
+
54
+ export default {
55
+ name: 'CnIndexPageWidgets',
56
+
57
+ components: {
58
+ CnKpiGrid,
59
+ CnWidgetWrapper,
60
+ },
61
+
62
+ inject: {
63
+ /** Consumer widget registry provided by CnAppRoot (custom over built-in). */
64
+ cnRegistry: { default: () => ({}) },
65
+ /** Host translate function provided by CnAppRoot. */
66
+ cnTranslate: { default: () => (key) => key },
67
+ },
68
+
69
+ props: {
70
+ /**
71
+ * Widget definitions `{ id, type, title?, content?, headerLink? }`.
72
+ * An entry without an id or with a type no layer resolves is skipped.
73
+ */
74
+ widgets: {
75
+ type: Array,
76
+ default: () => [],
77
+ },
78
+
79
+ /** Where the widgets sit: `stat-row` above the list, `side-panel` beside it. */
80
+ variant: {
81
+ type: String,
82
+ default: 'stat-row',
83
+ validator: (v) => ['stat-row', 'side-panel'].includes(v),
84
+ },
85
+ },
86
+
87
+ computed: {
88
+ /**
89
+ * The renderable entries, in the listed order.
90
+ *
91
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-stat-row-above-the-list
92
+ * @return {Array<{id: string, title: string, content: object, headerLink: (object|null), renderer: object}>}
93
+ */
94
+ entries() {
95
+ const list = Array.isArray(this.widgets) ? this.widgets : []
96
+ const out = []
97
+ for (const def of list) {
98
+ if (!def || typeof def.id !== 'string' || def.id === '' || typeof def.type !== 'string') {
99
+ continue
100
+ }
101
+ const renderer = this.resolveRenderer(def.type)
102
+ if (!renderer) {
103
+ if (process.env.NODE_ENV !== 'production') {
104
+ // eslint-disable-next-line no-console
105
+ console.warn(`[CnIndexPage] ${this.variant} widget "${def.id}" has type "${def.type}", which no widget registry resolves; skipped.`)
106
+ }
107
+ continue
108
+ }
109
+ const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
110
+ out.push({
111
+ id: def.id,
112
+ title: typeof def.title === 'string' && def.title !== '' ? translate(def.title) : '',
113
+ content: (def.content && typeof def.content === 'object') ? def.content : {},
114
+ headerLink: (def.headerLink && typeof def.headerLink === 'object') ? def.headerLink : null,
115
+ renderer,
116
+ })
117
+ }
118
+ return out
119
+ },
120
+ },
121
+
122
+ methods: {
123
+ /**
124
+ * Resolve a widget type: the consumer registry, then the dashboard
125
+ * catalog (the type as written, then its alias), then the built-ins.
126
+ *
127
+ * @param {string} type The widget type.
128
+ * @return {object|null} The component, or null.
129
+ */
130
+ resolveRenderer(type) {
131
+ const consumer = (this.cnRegistry || {})[type]
132
+ if (consumer) {
133
+ return consumer.component ?? consumer
134
+ }
135
+ const entry = getWidgetTypeEntry(type) || getWidgetTypeEntry(canonicalWidgetType(type))
136
+ if (entry && entry.renderer) {
137
+ return entry.renderer
138
+ }
139
+ return BUILT_IN_WIDGETS[type] || BUILT_IN_WIDGETS[canonicalWidgetType(type)] || null
140
+ },
141
+ },
142
+ }
143
+ </script>
144
+
145
+ <style scoped>
146
+ .cn-index-page-widgets--stat-row {
147
+ margin-bottom: var(--cn-board-section-gap, 20px);
148
+ }
149
+
150
+ .cn-index-page-widgets__cell {
151
+ display: flex;
152
+ min-width: 0;
153
+ }
154
+
155
+ .cn-index-page-widgets__cell > * {
156
+ flex: 1 1 auto;
157
+ min-width: 0;
158
+ }
159
+
160
+ /* The side panel column (DqTeamwachtrij: 300px, 16px between cards). Its
161
+ place beside the toolbar and the list is set by CnIndexPage
162
+ (`cn-index-page--with-side-panel`, index-page.css). */
163
+ .cn-index-page-widgets--side-panel {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 16px;
167
+ min-width: 0;
168
+ }
169
+ </style>