@conduction/nextcloud-vue 2.56.0-beta.5 → 2.56.0-beta.7

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 (239) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +39 -0
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +224 -9
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
  13. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
  15. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
  22. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
  23. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
  24. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
  25. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
  29. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  30. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
  31. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
  32. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
  33. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
  34. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
  35. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
  36. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
  37. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
  38. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
  39. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
  40. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
  41. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +300 -142
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
  47. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
  48. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
  49. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
  50. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
  51. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
  52. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
  53. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
  54. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
  55. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
  56. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
  57. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
  58. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +169 -12
  59. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
  61. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
  63. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
  64. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
  65. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
  71. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
  72. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
  73. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
  74. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
  75. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
  76. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
  77. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
  78. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
  79. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
  80. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
  81. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
  82. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
  83. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  84. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  85. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
  86. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
  87. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
  88. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
  89. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
  90. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
  91. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
  92. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
  93. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  94. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
  95. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  96. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +100 -3
  97. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  98. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
  100. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  101. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  102. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
  103. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
  104. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
  105. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
  106. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
  107. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
  108. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
  109. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
  110. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
  111. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
  112. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
  113. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
  114. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
  115. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
  116. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
  117. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
  118. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
  119. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
  120. package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
  121. package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
  122. package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
  123. package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
  124. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  125. package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
  126. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  127. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
  128. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  129. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
  130. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  131. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
  132. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
  133. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
  134. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
  135. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
  136. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
  137. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
  138. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
  139. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
  140. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
  141. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
  142. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
  143. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
  144. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  145. package/dist/esm/index.js +10 -0
  146. package/dist/esm/index.js.map +1 -1
  147. package/dist/esm/l10n/en.json.js +54 -1
  148. package/dist/esm/l10n/en.json.js.map +1 -1
  149. package/dist/esm/l10n/nl.json.js +54 -1
  150. package/dist/esm/l10n/nl.json.js.map +1 -1
  151. package/dist/esm/nextcloud-vue.css +931 -4
  152. package/dist/esm/utils/dateVariant.js +124 -0
  153. package/dist/esm/utils/dateVariant.js.map +1 -0
  154. package/dist/esm/utils/detailActionModel.js +260 -0
  155. package/dist/esm/utils/detailActionModel.js.map +1 -0
  156. package/dist/esm/utils/dueRule.js +84 -0
  157. package/dist/esm/utils/dueRule.js.map +1 -0
  158. package/dist/esm/utils/fetchAggregate.js +37 -1
  159. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  160. package/dist/esm/utils/fetchFilterCounts.js +186 -0
  161. package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
  162. package/dist/esm/utils/libraryWidgetKeys.js +5 -0
  163. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  164. package/dist/esm/utils/readPath.js +30 -0
  165. package/dist/esm/utils/readPath.js.map +1 -0
  166. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  167. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  168. package/dist/nextcloud-vue.cjs.js +15070 -8994
  169. package/dist/nextcloud-vue.cjs.js.map +2 -2
  170. package/dist/nextcloud-vue.css +931 -4
  171. package/l10n/en.json +54 -1
  172. package/l10n/nl.json +54 -1
  173. package/package.json +1 -1
  174. package/scripts/.jsdoc-baselines.json +3 -0
  175. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  176. package/src/components/CnAppNav/CnAppNav.md +26 -0
  177. package/src/components/CnAppNav/CnAppNav.vue +104 -1
  178. package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
  179. package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
  180. package/src/components/CnBoardView/CnBoardView.vue +77 -0
  181. package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
  182. package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
  183. package/src/components/CnBrandStripe/index.js +4 -0
  184. package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
  185. package/src/components/CnConversationThread/CnConversationThread.md +51 -0
  186. package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
  187. package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
  188. package/src/components/CnConversationThread/index.js +4 -0
  189. package/src/components/CnDetailPage/CnDetailPage.md +16 -0
  190. package/src/components/CnDetailPage/CnDetailPage.vue +683 -48
  191. package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
  192. package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
  193. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
  194. package/src/components/CnDocumentReviewList/index.js +4 -0
  195. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
  196. package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
  197. package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
  198. package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
  199. package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
  200. package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
  201. package/src/components/CnNextStepCard/index.js +4 -0
  202. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
  203. package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
  204. package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
  205. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
  206. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
  207. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +100 -3
  208. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
  209. package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
  210. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
  211. package/src/components/CnSegmentedControl/index.js +4 -0
  212. package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
  213. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
  214. package/src/components/CnStackedBarWidget/index.js +14 -0
  215. package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
  216. package/src/components/CnStackedBarWidgetForm/index.js +4 -0
  217. package/src/components/CnTabs/CnTab.vue +35 -0
  218. package/src/components/CnTabs/CnTabs.vue +244 -10
  219. package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
  220. package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
  221. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
  222. package/src/components/CnWeekStripWidget/index.js +14 -0
  223. package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
  224. package/src/components/CnWeekStripWidgetForm/index.js +4 -0
  225. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
  226. package/src/components/index.js +11 -0
  227. package/src/css/detail-page.css +47 -0
  228. package/src/index.js +9 -0
  229. package/src/schemas/app-manifest-v2.schema.json +389 -1
  230. package/src/schemas/app-manifest.schema.json +2 -1
  231. package/src/utils/dateVariant.js +120 -0
  232. package/src/utils/detailActionModel.js +256 -0
  233. package/src/utils/dueRule.js +94 -0
  234. package/src/utils/fetchAggregate.js +36 -0
  235. package/src/utils/fetchFilterCounts.js +182 -0
  236. package/src/utils/index.js +1 -0
  237. package/src/utils/libraryWidgetKeys.js +5 -0
  238. package/src/utils/readPath.js +27 -0
  239. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -0,0 +1,457 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <section
8
+ class="cn-conversation-thread"
9
+ data-testid="cn-conversation-thread"
10
+ :aria-labelledby="title ? titleId : null"
11
+ :aria-label="title ? null : fallbackLabel">
12
+ <component
13
+ :is="titleTag"
14
+ v-if="title"
15
+ :id="titleId"
16
+ class="cn-conversation-thread__title">
17
+ {{ title }}
18
+ </component>
19
+
20
+ <p v-if="rows.length === 0" class="cn-conversation-thread__empty" data-testid="cn-conversation-empty">
21
+ {{ emptyText }}
22
+ </p>
23
+
24
+ <ol v-else class="cn-conversation-thread__messages">
25
+ <li
26
+ v-for="row in rows"
27
+ :key="row.key"
28
+ class="cn-conversation-thread__message"
29
+ :class="`cn-conversation-thread__message--${row.side}`"
30
+ data-testid="cn-conversation-message"
31
+ :data-side="row.side">
32
+ <span class="cn-conversation-thread__avatar" aria-hidden="true">{{ row.initials }}</span>
33
+ <div class="cn-conversation-thread__bubble">
34
+ <span class="cn-conversation-thread__meta">
35
+ <span class="cn-conversation-thread__author">{{ row.author }}</span>
36
+ <time v-if="row.timeLabel" :datetime="row.datetime || null">{{ row.timeLabel }}</time>
37
+ </span>
38
+ <span class="cn-conversation-thread__text">{{ row.text }}</span>
39
+ </div>
40
+ </li>
41
+ </ol>
42
+
43
+ <form
44
+ v-if="allowReply"
45
+ class="cn-conversation-thread__reply"
46
+ data-testid="cn-conversation-reply"
47
+ @submit.prevent="onSend">
48
+ <label :for="inputId" class="cn-conversation-thread__reply-label">{{ replyLabel }}</label>
49
+ <textarea
50
+ :id="inputId"
51
+ v-model="draft"
52
+ class="cn-conversation-thread__input"
53
+ rows="2"
54
+ :placeholder="placeholder || null"
55
+ :disabled="sending"
56
+ :aria-describedby="errorMessage ? errorId : null"
57
+ data-testid="cn-conversation-input"
58
+ @keydown.ctrl.enter.prevent="onSend"
59
+ @keydown.meta.enter.prevent="onSend" />
60
+ <p
61
+ v-if="errorMessage"
62
+ :id="errorId"
63
+ class="cn-conversation-thread__error"
64
+ role="alert"
65
+ data-testid="cn-conversation-error">
66
+ {{ errorMessage }}
67
+ </p>
68
+ <div class="cn-conversation-thread__reply-actions">
69
+ <!-- @slot reply-actions Extra buttons before Send, for example "Save draft". -->
70
+ <!-- @binding {string} draft The text in the reply box. -->
71
+ <slot name="reply-actions" :draft="draft" />
72
+ <NcButton
73
+ variant="primary"
74
+ type="submit"
75
+ :disabled="!canSend"
76
+ data-testid="cn-conversation-send">
77
+ {{ sendLabel }}
78
+ </NcButton>
79
+ </div>
80
+ </form>
81
+ </section>
82
+ </template>
83
+
84
+ <script>
85
+ import axios from '@nextcloud/axios'
86
+ import { translate as t } from '@nextcloud/l10n'
87
+ import { NcButton } from '@nextcloud/vue'
88
+ import { prefixUrl } from '../../utils/headers.js'
89
+
90
+ let conversationThreadUid = 0
91
+
92
+ /**
93
+ * CnConversationThread: the messages between you and the other party, and a box to answer.
94
+ *
95
+ * Each message has an author, a time and a side: `us` for the organisation,
96
+ * `them` for the resident or customer. The reply box emits `send`, and also
97
+ * posts to `endpoint` when one is set. It needs no Talk and no other app:
98
+ * where the messages are stored is the host's business.
99
+ *
100
+ * ```vue
101
+ * <CnConversationThread
102
+ * title="Contact with the resident"
103
+ * reply-label="Answer to the resident"
104
+ * :messages="[
105
+ * { id: 1, author: 'Sanne de Vries', time: '2026-10-04T10:52:00', side: 'them',
106
+ * text: 'Thank you. I would like to see all documents.' },
107
+ * { id: 2, time: '2026-10-04T10:24:00', side: 'us',
108
+ * text: 'Construction starts in March.' },
109
+ * ]"
110
+ * @send="saveReply" />
111
+ * ```
112
+ *
113
+ * The `conversation` detail widget type renders this thread from a field on
114
+ * the record.
115
+ */
116
+ export default {
117
+ name: 'CnConversationThread',
118
+
119
+ components: {
120
+ NcButton,
121
+ },
122
+
123
+ props: {
124
+ /**
125
+ * The messages, oldest first: `{ id?, author?, time?, text, side? }`.
126
+ * `side` is `us` or `them` (default `them`). `time` is an ISO
127
+ * date, or any text to show as is.
128
+ */
129
+ messages: {
130
+ type: Array,
131
+ default: () => [],
132
+ },
133
+
134
+ /** Heading of the thread. */
135
+ title: {
136
+ type: String,
137
+ default: '',
138
+ },
139
+
140
+ /** Element the heading renders as. */
141
+ titleTag: {
142
+ type: String,
143
+ default: 'h3',
144
+ },
145
+
146
+ /** Whether the reply box renders. Turn it off for a closed or read-only record. */
147
+ allowReply: {
148
+ type: Boolean,
149
+ default: true,
150
+ },
151
+
152
+ /** Label of the reply box. */
153
+ replyLabel: {
154
+ type: String,
155
+ default: () => t('nextcloud-vue', 'Reply'),
156
+ },
157
+
158
+ /** Placeholder inside the reply box. */
159
+ placeholder: {
160
+ type: String,
161
+ default: '',
162
+ },
163
+
164
+ /** Label of the send button. */
165
+ sendLabel: {
166
+ type: String,
167
+ default: () => t('nextcloud-vue', 'Send'),
168
+ },
169
+
170
+ /** Author shown on an `us` message that names none. */
171
+ usLabel: {
172
+ type: String,
173
+ default: () => t('nextcloud-vue', 'You'),
174
+ },
175
+
176
+ /**
177
+ * URL the reply is POSTed to as `{ message }`. Empty means the host
178
+ * handles `send` itself and nothing is requested.
179
+ */
180
+ endpoint: {
181
+ type: String,
182
+ default: '',
183
+ },
184
+
185
+ /** Extra fields sent along with `message` when `endpoint` is set. */
186
+ payload: {
187
+ type: Object,
188
+ default: () => ({}),
189
+ },
190
+
191
+ /** Shown when there are no messages yet. */
192
+ emptyText: {
193
+ type: String,
194
+ default: () => t('nextcloud-vue', 'No messages yet.'),
195
+ },
196
+ },
197
+
198
+ emits: [
199
+ /** A reply was submitted. Payload: the text. */
200
+ 'send',
201
+ /** The endpoint accepted the reply. Payload: the response body. */
202
+ 'sent',
203
+ /** The endpoint refused the reply. Payload: the error. */
204
+ 'error',
205
+ ],
206
+
207
+ data() {
208
+ conversationThreadUid += 1
209
+ return {
210
+ draft: '',
211
+ sending: false,
212
+ errorMessage: '',
213
+ titleId: `cn-conversation-title-${conversationThreadUid}`,
214
+ inputId: `cn-conversation-input-${conversationThreadUid}`,
215
+ errorId: `cn-conversation-error-${conversationThreadUid}`,
216
+ }
217
+ },
218
+
219
+ computed: {
220
+ /**
221
+ * The messages, with author, side, time and initials resolved.
222
+ *
223
+ * @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string}>} The rows.
224
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
225
+ */
226
+ rows() {
227
+ return (Array.isArray(this.messages) ? this.messages : [])
228
+ .filter((message) => message && typeof message === 'object')
229
+ .map((message, index) => {
230
+ const side = message.side === 'us' ? 'us' : 'them'
231
+ const author = typeof message.author === 'string' && message.author !== ''
232
+ ? message.author
233
+ : (side === 'us' ? this.usLabel : '')
234
+ const time = this.formatTime(message.time)
235
+ return {
236
+ key: message.id === undefined || message.id === null ? `message-${index}` : String(message.id),
237
+ author,
238
+ initials: this.initialsOf(author),
239
+ side,
240
+ text: message.text === undefined || message.text === null ? '' : String(message.text),
241
+ timeLabel: time.label,
242
+ datetime: time.datetime,
243
+ }
244
+ })
245
+ },
246
+
247
+ /**
248
+ * Whether there is something to send and nothing in flight.
249
+ *
250
+ * @return {boolean} True when Send is enabled.
251
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
252
+ */
253
+ canSend() {
254
+ return !this.sending && this.draft.trim() !== ''
255
+ },
256
+
257
+ /**
258
+ * Accessible name for a thread without a title.
259
+ *
260
+ * @return {string} The label.
261
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
262
+ */
263
+ fallbackLabel() {
264
+ return t('nextcloud-vue', 'Conversation')
265
+ },
266
+ },
267
+
268
+ methods: {
269
+ /**
270
+ * Up to two initials of a name, for the avatar.
271
+ *
272
+ * @param {string} name The author.
273
+ * @return {string} The initials, upper case.
274
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
275
+ */
276
+ initialsOf(name) {
277
+ const parts = String(name || '').trim().split(/\s+/).filter(Boolean)
278
+ if (parts.length === 0) {
279
+ return ''
280
+ }
281
+ const first = parts[0].charAt(0)
282
+ const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : ''
283
+ return (first + last).toUpperCase()
284
+ },
285
+
286
+ /**
287
+ * Turn a message time into what the reader sees and what `<time>` carries.
288
+ *
289
+ * @param {unknown} value An ISO date, a Date, or text to show as is.
290
+ * @return {{label: string, datetime: string}} The visible label and the machine value.
291
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
292
+ */
293
+ formatTime(value) {
294
+ if (value === null || value === undefined || value === '') {
295
+ return { label: '', datetime: '' }
296
+ }
297
+ const date = value instanceof Date ? value : new Date(value)
298
+ const looksLikeDate = value instanceof Date || typeof value === 'number' || /^\d{4}-\d{2}-\d{2}/.test(String(value))
299
+ if (!looksLikeDate || Number.isNaN(date.getTime())) {
300
+ return { label: String(value), datetime: '' }
301
+ }
302
+ const label = date.toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })
303
+ return { label, datetime: date.toISOString() }
304
+ },
305
+
306
+ /**
307
+ * Submit the reply: tell the host, and post it when an endpoint is set.
308
+ *
309
+ * Without an endpoint the box clears straight away, because the host
310
+ * owns the send. With one it clears only when the request succeeds, so
311
+ * a refused reply is still there to try again.
312
+ *
313
+ * @return {Promise<void>}
314
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
315
+ */
316
+ async onSend() {
317
+ if (!this.canSend) {
318
+ return
319
+ }
320
+ const text = this.draft.trim()
321
+ this.errorMessage = ''
322
+ this.$emit('send', text)
323
+ if (!this.endpoint) {
324
+ this.draft = ''
325
+ return
326
+ }
327
+ this.sending = true
328
+ try {
329
+ const response = await axios.post(prefixUrl(this.endpoint), { ...this.payload, message: text })
330
+ this.draft = ''
331
+ this.$emit('sent', response?.data)
332
+ } catch (error) {
333
+ this.errorMessage = t('nextcloud-vue', 'The message could not be sent. Try again.')
334
+ this.$emit('error', error)
335
+ } finally {
336
+ this.sending = false
337
+ }
338
+ },
339
+ },
340
+ }
341
+ </script>
342
+
343
+ <style scoped>
344
+ .cn-conversation-thread {
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: calc(3.5 * var(--default-grid-baseline));
348
+ }
349
+
350
+ .cn-conversation-thread__title {
351
+ font-size: 1.1em;
352
+ font-weight: 700;
353
+ margin: 0;
354
+ }
355
+
356
+ .cn-conversation-thread__empty {
357
+ color: var(--color-text-maxcontrast);
358
+ margin: 0;
359
+ }
360
+
361
+ .cn-conversation-thread__messages {
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: calc(3.5 * var(--default-grid-baseline));
365
+ list-style: none;
366
+ margin: 0;
367
+ padding: 0;
368
+ }
369
+
370
+ .cn-conversation-thread__message {
371
+ display: flex;
372
+ gap: calc(3 * var(--default-grid-baseline));
373
+ }
374
+
375
+ .cn-conversation-thread__message--us {
376
+ flex-direction: row-reverse;
377
+ }
378
+
379
+ .cn-conversation-thread__avatar {
380
+ align-items: center;
381
+ background: var(--color-background-dark);
382
+ border-radius: 50%;
383
+ color: var(--color-main-text);
384
+ display: flex;
385
+ flex: none;
386
+ font-size: 0.8em;
387
+ font-weight: 700;
388
+ height: 34px;
389
+ justify-content: center;
390
+ width: 34px;
391
+ }
392
+
393
+ .cn-conversation-thread__bubble {
394
+ background: var(--color-background-hover);
395
+ border-radius: var(--border-radius-large);
396
+ border-start-start-radius: var(--border-radius-small, 4px);
397
+ color: var(--color-main-text);
398
+ display: flex;
399
+ flex: 1;
400
+ flex-direction: column;
401
+ gap: var(--default-grid-baseline);
402
+ min-width: 0;
403
+ padding: calc(3 * var(--default-grid-baseline)) calc(3.5 * var(--default-grid-baseline));
404
+ }
405
+
406
+ .cn-conversation-thread__message--us .cn-conversation-thread__bubble {
407
+ background: var(--color-primary-element-light);
408
+ border-start-end-radius: var(--border-radius-small, 4px);
409
+ border-start-start-radius: var(--border-radius-large);
410
+ color: var(--color-primary-element-light-text);
411
+ }
412
+
413
+ .cn-conversation-thread__meta {
414
+ display: flex;
415
+ flex-wrap: wrap;
416
+ font-size: 0.9em;
417
+ gap: calc(2 * var(--default-grid-baseline));
418
+ }
419
+
420
+ .cn-conversation-thread__author {
421
+ font-weight: 600;
422
+ }
423
+
424
+ .cn-conversation-thread__text {
425
+ overflow-wrap: anywhere;
426
+ white-space: pre-wrap;
427
+ }
428
+
429
+ .cn-conversation-thread__reply {
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: calc(2 * var(--default-grid-baseline));
433
+ }
434
+
435
+ .cn-conversation-thread__reply-label {
436
+ font-weight: 600;
437
+ }
438
+
439
+ .cn-conversation-thread__input {
440
+ box-sizing: border-box;
441
+ margin: 0;
442
+ resize: vertical;
443
+ width: 100%;
444
+ }
445
+
446
+ .cn-conversation-thread__error {
447
+ color: var(--color-error-text, var(--color-error));
448
+ margin: 0;
449
+ }
450
+
451
+ .cn-conversation-thread__reply-actions {
452
+ display: flex;
453
+ flex-wrap: wrap;
454
+ gap: calc(2.5 * var(--default-grid-baseline));
455
+ justify-content: flex-end;
456
+ }
457
+ </style>
@@ -0,0 +1,144 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-case-widget-card">
8
+ <CnConversationThread
9
+ :messages="messages"
10
+ :title="content.title || ''"
11
+ :allowReply="content.allowReply !== false"
12
+ :replyLabel="content.replyLabel || undefined"
13
+ :placeholder="content.placeholder || ''"
14
+ :sendLabel="content.sendLabel || undefined"
15
+ :endpoint="endpoint"
16
+ :emptyText="content.emptyText || undefined"
17
+ @sent="onSent" />
18
+ </div>
19
+ </template>
20
+
21
+ <script>
22
+ import CnConversationThread from './CnConversationThread.vue'
23
+ import { readPath } from '../../utils/readPath.js'
24
+
25
+ /**
26
+ * CnConversationWidget: the `conversation` detail widget type.
27
+ *
28
+ * Renders CnConversationThread from a list on the bound record:
29
+ * `content: { field, endpoint?, authorField?, timeField?, textField?, sideField?, usValue? }`.
30
+ * `@objectId` in `endpoint` is replaced by the record's id. A reply the
31
+ * endpoint accepts is shown at once, until the record is read again.
32
+ * Resolved by its registry key, not exported.
33
+ */
34
+ export default {
35
+ name: 'CnConversationWidget',
36
+
37
+ components: {
38
+ CnConversationThread,
39
+ },
40
+
41
+ props: {
42
+ /** The widget's content. `field` is the dot-path to the messages on the record. */
43
+ content: {
44
+ type: Object,
45
+ default: () => ({}),
46
+ },
47
+
48
+ /** The bound record. */
49
+ objectData: {
50
+ type: Object,
51
+ default: null,
52
+ },
53
+
54
+ /** The bound record's id. */
55
+ objectId: {
56
+ type: [String, Number],
57
+ default: '',
58
+ },
59
+ },
60
+
61
+ data() {
62
+ return {
63
+ /** Replies sent from this widget that the record does not hold yet. */
64
+ sentHere: [],
65
+ }
66
+ },
67
+
68
+ computed: {
69
+ /**
70
+ * The record's messages in the thread's shape, plus the replies sent here.
71
+ *
72
+ * @return {Array<object>} The messages.
73
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
74
+ */
75
+ messages() {
76
+ const c = this.content
77
+ const value = readPath(this.objectData || {}, c.field || 'messages')
78
+ const usValue = String(c.usValue === undefined ? 'us' : c.usValue)
79
+ const stored = (Array.isArray(value) ? value : [])
80
+ .filter((message) => message && typeof message === 'object')
81
+ .map((message) => ({
82
+ id: message.id,
83
+ author: readPath(message, c.authorField || 'author'),
84
+ time: readPath(message, c.timeField || 'time'),
85
+ text: readPath(message, c.textField || 'text'),
86
+ side: String(readPath(message, c.sideField || 'side')) === usValue ? 'us' : 'them',
87
+ }))
88
+ return [...stored, ...this.sentHere]
89
+ },
90
+
91
+ /**
92
+ * The endpoint with the record's id filled in.
93
+ *
94
+ * @return {string} The URL, or '' when the content names none.
95
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
96
+ */
97
+ endpoint() {
98
+ const url = this.content.endpoint
99
+ if (typeof url !== 'string' || url === '') {
100
+ return ''
101
+ }
102
+ return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
103
+ },
104
+ },
105
+
106
+ watch: {
107
+ // The record was read again, so it now holds what was sent from here.
108
+ objectData() {
109
+ this.sentHere = []
110
+ },
111
+ },
112
+
113
+ methods: {
114
+ /**
115
+ * Show an accepted reply at once, without waiting for the record.
116
+ *
117
+ * @param {object} data The endpoint's response.
118
+ * @return {void}
119
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
120
+ */
121
+ onSent(data) {
122
+ const text = data?.message ?? data?.text
123
+ if (typeof text !== 'string' || text === '') {
124
+ return
125
+ }
126
+ this.sentHere.push({
127
+ id: `sent-${this.sentHere.length}`,
128
+ time: new Date().toISOString(),
129
+ text,
130
+ side: 'us',
131
+ })
132
+ },
133
+ },
134
+ }
135
+ </script>
136
+
137
+ <style scoped>
138
+ .cn-case-widget-card {
139
+ background: var(--color-main-background);
140
+ border: 1px solid var(--color-border);
141
+ border-radius: var(--border-radius-large);
142
+ padding: calc(5 * var(--default-grid-baseline));
143
+ }
144
+ </style>
@@ -0,0 +1,4 @@
1
+ import CnConversationThread from './CnConversationThread.vue'
2
+
3
+ export default CnConversationThread
4
+ export { CnConversationThread }
@@ -405,6 +405,22 @@ The page header's overflow menu carries Refresh plus the mandatory trio Request
405
405
  | `showReportBug` | Boolean | `true` | Whether the Report-a-bug entry renders in the page-header menu. |
406
406
  | `showDocumentation` | Boolean | `true` | Whether the Documentation entry renders. Its target is resolved by the shared menu from the app-wide documentation base plus this page's id, so leaving it on costs the host nothing. |
407
407
 
408
+ ## The action model
409
+
410
+ Four levels of action and the case surfaces around them. All opt in. See [the reference page](../../../docs/components/cn-detail-page.md#the-action-model) for manifest examples.
411
+
412
+ | Prop | Type | Default | Description |
413
+ |---|---|---|---|
414
+ | `primaryActionByStage` | Object \| null | `null` | Map from a stage value to the primary action: an action object or the id of a header action. Falls back to `primaryAction`. |
415
+ | `stageField` | String | `'status'` | Dot-path to the record's stage. |
416
+ | `quickActions` | Array | `[]` | At most three always visible header buttons. |
417
+ | `actionsMenu` | Object \| null | `null` | `{ showRefresh?, showHelpLinks?, label? }` for the overflow menu. |
418
+ | `nextStep` | Object \| null | `null` | The "what now" card per stage, rendered by `CnNextStepCard`. |
419
+ | `typePill` | Object \| null | `null` | `{ field, colorMap?, labels?, variant? }`, a pill above the title. |
420
+ | `statusPill` | Object \| null | `null` | A second pill, same shape. |
421
+ | `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. |
422
+ | `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
423
+
408
424
  ## The record as a place
409
425
 
410
426
  `listNavigation`, `primaryAction` and `tabInAddress` make a record somewhere a handler stays. All three are off by default. See [the reference page](../../../docs/components/cn-detail-page.md#the-record-as-a-place).