@conduction/nextcloud-vue 2.80.0 → 2.81.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 (290) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +140 -10
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +52 -16
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -3
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
  15. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
  17. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
  18. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
  19. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
  20. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
  21. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
  22. package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
  23. package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
  24. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
  25. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
  26. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
  27. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
  28. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
  29. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
  30. package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
  31. package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
  32. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  33. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  36. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  38. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +65 -5
  39. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
  41. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  43. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
  45. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +72 -0
  47. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  48. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +21 -4
  49. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  50. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  51. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  52. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  54. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  56. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +131 -0
  57. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +80 -5
  60. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
  62. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  64. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  66. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  67. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  68. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  69. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  70. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  71. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
  72. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  73. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  74. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  75. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -2
  76. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
  78. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  79. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  80. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  81. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  82. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  83. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  84. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
  85. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  86. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
  87. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  89. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  90. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  91. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  92. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  93. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  94. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  95. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  96. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
  97. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
  98. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
  99. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
  100. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
  101. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
  102. package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
  103. package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
  104. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +30 -6
  105. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  106. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
  107. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
  109. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
  110. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
  111. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
  112. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
  113. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
  114. package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
  115. package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
  116. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  117. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  118. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
  119. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
  120. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
  121. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
  122. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
  123. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
  124. package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
  125. package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
  126. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
  127. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  128. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  129. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  130. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  131. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  132. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  133. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
  134. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
  135. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
  136. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
  137. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
  138. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
  139. package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
  140. package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
  141. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
  142. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  143. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
  144. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  145. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
  146. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  147. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
  148. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  149. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
  150. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
  151. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
  152. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
  153. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
  154. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
  155. package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
  156. package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
  157. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  158. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  159. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  160. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  161. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  162. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  163. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  164. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
  165. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  166. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +96 -50
  167. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  168. package/dist/esm/composables/useListView.js +4 -4
  169. package/dist/esm/composables/useListView.js.map +1 -1
  170. package/dist/esm/index.js +17 -1
  171. package/dist/esm/index.js.map +1 -1
  172. package/dist/esm/l10n/en.json.js +9 -1
  173. package/dist/esm/l10n/en.json.js.map +1 -1
  174. package/dist/esm/l10n/nl.json.js +55 -1
  175. package/dist/esm/l10n/nl.json.js.map +1 -1
  176. package/dist/esm/nextcloud-vue.css +1041 -12
  177. package/dist/esm/utils/contentHeaderLink.js +51 -0
  178. package/dist/esm/utils/contentHeaderLink.js.map +1 -0
  179. package/dist/esm/utils/facets.js +75 -3
  180. package/dist/esm/utils/facets.js.map +1 -1
  181. package/dist/esm/utils/fetchFilterCounts.js +40 -5
  182. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  183. package/dist/esm/utils/fillTemplate.js +7 -1
  184. package/dist/esm/utils/fillTemplate.js.map +1 -1
  185. package/dist/esm/utils/initials.js +32 -0
  186. package/dist/esm/utils/initials.js.map +1 -0
  187. package/dist/esm/utils/libraryWidgetKeys.js +7 -0
  188. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  189. package/dist/esm/utils/objectWrite.js +59 -0
  190. package/dist/esm/utils/objectWrite.js.map +1 -0
  191. package/dist/esm/utils/pageControls.js +81 -0
  192. package/dist/esm/utils/pageControls.js.map +1 -0
  193. package/dist/esm/utils/personalLenses.js +40 -3
  194. package/dist/esm/utils/personalLenses.js.map +1 -1
  195. package/dist/esm/utils/resolveFilterTokens.js +1 -1
  196. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  197. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  198. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  199. package/dist/esm/utils/sentinelTokens.js +19 -1
  200. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  201. package/dist/esm/utils/userGroupsPeek.js +91 -0
  202. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  203. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  204. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  205. package/dist/esm/utils/visibleWhen.js +57 -5
  206. package/dist/esm/utils/visibleWhen.js.map +1 -1
  207. package/dist/esm/utils/widgetDispatch.js +1 -1
  208. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  209. package/dist/esm/utils/widgetVisibility.js +4 -57
  210. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  211. package/dist/nextcloud-vue.cjs.js +13589 -7833
  212. package/dist/nextcloud-vue.cjs.js.map +2 -2
  213. package/dist/nextcloud-vue.css +1041 -12
  214. package/l10n/en.json +9 -1
  215. package/l10n/nl.json +55 -1
  216. package/package.json +2 -1
  217. package/scripts/check-docs-mdx.mjs +108 -0
  218. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  219. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  220. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  221. package/src/components/CnBannerWidget/CnBannerWidget.vue +205 -20
  222. package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
  223. package/src/components/CnCellRenderer/CnCellRenderer.vue +47 -4
  224. package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
  225. package/src/components/CnChipsFieldWidget/index.js +25 -0
  226. package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
  227. package/src/components/CnChoicePickerWidget/index.js +26 -0
  228. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  229. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  230. package/src/components/CnDashboardPage/CnDashboardPage.vue +78 -5
  231. package/src/components/CnDataTable/CnDataTable.vue +21 -12
  232. package/src/components/CnDetailPage/CnDetailPage.vue +89 -3
  233. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  234. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  235. package/src/components/CnFactsWidget/CnFactsWidget.vue +142 -2
  236. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +268 -6
  237. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  238. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  239. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  240. package/src/components/CnFormPage/CnFormPage.vue +81 -0
  241. package/src/components/CnIndexPage/CnIndexPage.vue +56 -4
  242. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  243. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  244. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
  245. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  246. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  247. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
  248. package/src/components/CnRadioCardsWidget/index.js +27 -0
  249. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +55 -6
  250. package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
  251. package/src/components/CnSavedViewsWidget/index.js +29 -0
  252. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
  253. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
  254. package/src/components/CnSettingsFormWidget/index.js +26 -0
  255. package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
  256. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  257. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
  258. package/src/components/CnSwitchRowsWidget/index.js +25 -0
  259. package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
  260. package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
  261. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
  262. package/src/components/CnToggleTableWidget/index.js +26 -0
  263. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  264. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  265. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  266. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +309 -11
  267. package/src/components/index.js +15 -0
  268. package/src/composables/useListView.js +4 -4
  269. package/src/css/look-board-detail.css +36 -2
  270. package/src/css/look-board-index.css +39 -8
  271. package/src/css/look-board.css +72 -0
  272. package/src/index.js +15 -0
  273. package/src/schemas/app-manifest-v2.schema.json +93 -9
  274. package/src/utils/contentHeaderLink.js +48 -0
  275. package/src/utils/facets.js +74 -2
  276. package/src/utils/fetchFilterCounts.js +39 -4
  277. package/src/utils/fillTemplate.js +7 -1
  278. package/src/utils/initials.js +29 -0
  279. package/src/utils/libraryWidgetKeys.js +7 -0
  280. package/src/utils/objectWrite.js +55 -0
  281. package/src/utils/pageControls.js +78 -0
  282. package/src/utils/personalLenses.js +39 -2
  283. package/src/utils/resolveFilterTokens.js +3 -1
  284. package/src/utils/resolveManifestTokens.js +2 -0
  285. package/src/utils/sentinelTokens.js +19 -1
  286. package/src/utils/userGroupsPeek.js +87 -0
  287. package/src/utils/validateManifestV2.compiled.js +3 -3
  288. package/src/utils/visibleWhen.js +56 -4
  289. package/src/utils/widgetDispatch.js +1 -1
  290. package/src/utils/widgetVisibility.js +7 -56
@@ -16,6 +16,13 @@
16
16
  :rowActions="rowActions"
17
17
  :newActions="newActions"
18
18
  :linkedItems="linkedItems"
19
+ :columns="columns"
20
+ :rowData="rowDataSource"
21
+ :preferenceApp="preferenceApp"
22
+ :preferenceKey="preferenceKey"
23
+ :bulkActions="bulkActions"
24
+ :groupBy="groupBy"
25
+ :facets="facets"
19
26
  @changed="onBrowserChanged" />
20
27
  <template v-else>
21
28
  <!-- Upload error -->
@@ -240,7 +247,7 @@ import CnFilePreview from '../CnFilePreview/CnFilePreview.vue'
240
247
  import CnFilesBrowser from '../CnFilesBrowser/CnFilesBrowser.vue'
241
248
  import { useFileOpener } from '../../composables/useFileOpener.js'
242
249
  import { buildHeaders, prefixUrl } from '../../utils/index.js'
243
- import { objectDisplayName, resolveObjectFolderInfo } from '../CnFilesBrowser/filesBrowser.js'
250
+ import { indexRowData, objectDisplayName, resolveObjectFolderInfo } from '../CnFilesBrowser/filesBrowser.js'
244
251
  import { createOpenRegisterSource } from '../CnFilesBrowser/openRegisterSource.js'
245
252
 
246
253
  /**
@@ -359,6 +366,47 @@ export default {
359
366
  * Null fetches nothing.
360
367
  */
361
368
  linkedItemsUrl: { type: String, default: null },
369
+ /**
370
+ * Forwarded to CnFilesBrowser: the columns the browser shows, built-in
371
+ * names or declared columns (`{ key, label, source, formatter }`).
372
+ * A `source: 'row'` column reads the host's record for the file from
373
+ * `rowDataUrl`. None keeps name, size and modified.
374
+ *
375
+ * @type {Array<string|object>}
376
+ */
377
+ columns: { type: Array, default: () => [] },
378
+ /** Forwarded to CnFilesBrowser: the app id the user's column choice is stored under; unset offers no Columns chooser. */
379
+ preferenceApp: { type: String, default: '' },
380
+ /** Forwarded to CnFilesBrowser: the key the column choice is stored under. */
381
+ preferenceKey: { type: String, default: 'files-browser-columns' },
382
+ /**
383
+ * An app endpoint that answers the host's own record per file, for the
384
+ * browser's `source: 'row'` columns. App-relative; `{objectId}` is
385
+ * replaced. Read once per listing. The answer is a list of records
386
+ * (or the list at `rowDataPath`, or under `items` or `results`), each
387
+ * naming its file in `rowDataKey`. Null reads nothing.
388
+ */
389
+ rowDataUrl: { type: String, default: null },
390
+ /**
391
+ * Forwarded to CnFilesBrowser: the host's actions on a selection of
392
+ * files. With at least one, files get checkboxes and a bulk bar.
393
+ *
394
+ * @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object}>}
395
+ */
396
+ bulkActions: { type: Array, default: () => [] },
397
+ /** Forwarded to CnFilesBrowser: the key of a declared column to group the files on. */
398
+ groupBy: { type: String, default: '' },
399
+ /**
400
+ * Forwarded to CnFilesBrowser: keys of declared columns to filter on,
401
+ * a chip per value in use.
402
+ *
403
+ * @type {string[]}
404
+ */
405
+ facets: { type: Array, default: () => [] },
406
+ /** A dotted path to the list of records in the `rowDataUrl` answer; empty reads the answer itself. */
407
+ rowDataPath: { type: String, default: '' },
408
+ /** The record field that holds the Nextcloud file id. */
409
+ rowDataKey: { type: String, default: 'fileId' },
362
410
  /** Label of the Download action. */
363
411
  downloadLabel: { type: String, default: () => t('nextcloud-vue', 'Download') },
364
412
  /** Label of the action that opens the Files sidebar on the file. */
@@ -424,6 +472,27 @@ export default {
424
472
  },
425
473
 
426
474
  computed: {
475
+ /**
476
+ * The browser's `rowData`: a function that reads the host's records
477
+ * from `rowDataUrl` and keys them by file id, or null without a url.
478
+ * The browser calls it once per listing.
479
+ *
480
+ * @return {(function(): Promise<object>)|null} The row data source.
481
+ * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
482
+ */
483
+ rowDataSource() {
484
+ if (!this.rowDataUrl || !this.objectId) {
485
+ return null
486
+ }
487
+ const url = generateUrl(this.rowDataUrl.replace('{objectId}', encodeURIComponent(this.objectId)))
488
+ const path = this.rowDataPath
489
+ const key = this.rowDataKey
490
+ return async () => {
491
+ const { data } = await axios.get(url)
492
+ return indexRowData(data, { path, key })
493
+ }
494
+ },
495
+
427
496
  /**
428
497
  * The root crumb: the host's label, else the object's name (its folder
429
498
  * on disk is called after its uuid), else the folder's own name.
@@ -18,6 +18,8 @@
18
18
  {{ cancelLabel }}
19
19
  </NcButton>
20
20
  </p>
21
+ <!-- @slot composer-before An app's own control above the composer, e.g. a template picker. Scope: `{ setText, text }`; `setText(text)` fills the composer, `text` is what it holds now. -->
22
+ <slot name="composer-before" :setText="setComposerText" :text="newNoteText" />
21
23
  <CnNoteComposer
22
24
  class="cn-sidebar-tab__composer"
23
25
  :modelValue="newNoteText"
@@ -481,6 +483,20 @@ export default {
481
483
  }
482
484
  },
483
485
 
486
+ /**
487
+ * Fill the composer from outside, for the `composer-before` slot.
488
+ *
489
+ * The app decides what goes in (a template body, a quoted line); the
490
+ * composer stays the one place the text lives, so sending, editing and
491
+ * the mention picker all see it exactly as if it had been typed.
492
+ *
493
+ * @param {string} text The text the composer should hold.
494
+ * @return {void}
495
+ */
496
+ setComposerText(text) {
497
+ this.newNoteText = (text === null || text === undefined) ? '' : String(text)
498
+ },
499
+
484
500
  startEdit(note) {
485
501
  this.replyTo = null
486
502
  this.editingNoteId = note.id
@@ -0,0 +1,313 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <fieldset class="cn-radio-cards" data-testid="cn-radio-cards" :aria-busy="busy ? 'true' : 'false'">
8
+ <legend v-if="label" class="cn-radio-cards__legend">
9
+ {{ label }}
10
+ </legend>
11
+ <div class="cn-radio-cards__grid" :class="`cn-radio-cards__grid--${columns}`">
12
+ <label
13
+ v-for="option in options"
14
+ :key="String(option.value)"
15
+ class="cn-radio-cards__card"
16
+ :class="{ 'cn-radio-cards__card--selected': isSelected(option) }"
17
+ :data-testid="`cn-radio-cards-option-${option.value}`">
18
+ <input
19
+ class="cn-radio-cards__input"
20
+ type="radio"
21
+ :name="groupName"
22
+ :value="String(option.value)"
23
+ :checked="isSelected(option)"
24
+ :disabled="readOnly || busy"
25
+ @change="choose(option)">
26
+ <span class="cn-radio-cards__dot" aria-hidden="true" />
27
+ <span class="cn-radio-cards__text">
28
+ <span class="cn-radio-cards__title">{{ option.label }}</span>
29
+ <span v-if="option.description" class="cn-radio-cards__description">{{ option.description }}</span>
30
+ </span>
31
+ </label>
32
+ </div>
33
+ <NcNoteCard v-if="notice" type="error" data-testid="cn-radio-cards-notice">
34
+ {{ notice }}
35
+ </NcNoteCard>
36
+ </fieldset>
37
+ </template>
38
+
39
+ <script>
40
+ import { NcNoteCard } from '@nextcloud/vue'
41
+ import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
42
+ import { readPath } from '../../utils/readPath.js'
43
+ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
44
+
45
+ let uid = 0
46
+
47
+ /**
48
+ * CnRadioCardsWidget — the `radio-cards` widget: one choice out of a few cards,
49
+ * each with a title and a description, bound to an enum field of the CURRENT
50
+ * object. Choosing a card saves at once with ONE PATCH of that field; a refused
51
+ * PATCH puts the choice back and shows the server's words. Real radio inputs
52
+ * underneath, so the choice is perceivable without colour and works from the
53
+ * keyboard.
54
+ *
55
+ * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md
56
+ */
57
+ export default {
58
+ name: 'CnRadioCardsWidget',
59
+
60
+ components: { NcNoteCard },
61
+
62
+ inject: {
63
+ cnTranslate: { default: () => (key) => key },
64
+ },
65
+
66
+ props: {
67
+ /**
68
+ * The widget configuration.
69
+ *
70
+ * - `field` (required) — the top-level enum field.
71
+ * - `options` (required) — `[{ value, label, description? }]`.
72
+ * - `label` — a legend over the cards (visually shown).
73
+ * - `columns` — 1, 2 or 3 (default 2).
74
+ * - `readOnly`.
75
+ *
76
+ * @type {object}
77
+ */
78
+ content: {
79
+ type: Object,
80
+ default: () => ({}),
81
+ },
82
+
83
+ /** The current detail-page object. */
84
+ objectData: {
85
+ type: Object,
86
+ default: null,
87
+ },
88
+
89
+ /** The current object id. */
90
+ objectId: {
91
+ type: [String, Number],
92
+ default: '',
93
+ },
94
+
95
+ /** OpenRegister register slug of the surface. */
96
+ register: {
97
+ type: [String, Object],
98
+ default: '',
99
+ },
100
+
101
+ /** OpenRegister schema slug of the surface. */
102
+ schema: {
103
+ type: [String, Object],
104
+ default: '',
105
+ },
106
+ },
107
+
108
+ emits: ['saved'],
109
+
110
+ data() {
111
+ uid += 1
112
+ return {
113
+ groupName: `cn-radio-cards-${uid}`,
114
+ saved: '',
115
+ value: '',
116
+ busy: false,
117
+ notice: '',
118
+ }
119
+ },
120
+
121
+ computed: {
122
+ options() {
123
+ const list = Array.isArray(this.content.options) ? this.content.options : []
124
+ return list
125
+ .filter((option) => option !== null && option !== undefined)
126
+ .map((option) => (typeof option === 'object'
127
+ ? { value: option.value, label: this.tr(String(option.label ?? option.value)), description: this.tr(option.description || '') }
128
+ : { value: option, label: this.tr(String(option)), description: '' }))
129
+ },
130
+
131
+ label() {
132
+ return this.tr(this.content.label || '')
133
+ },
134
+
135
+ columns() {
136
+ return [1, 3].includes(this.content.columns) ? this.content.columns : 2
137
+ },
138
+
139
+ readOnly() {
140
+ return this.content.readOnly === true
141
+ },
142
+
143
+ objectUrl() {
144
+ return objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))
145
+ },
146
+ },
147
+
148
+ watch: {
149
+ objectData() {
150
+ this.adopt()
151
+ },
152
+ },
153
+
154
+ created() {
155
+ this.adopt()
156
+ },
157
+
158
+ methods: {
159
+ tr(text) {
160
+ if (typeof text !== 'string' || text === '') {
161
+ return ''
162
+ }
163
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
164
+ },
165
+
166
+ /** Read the choice off the object. */
167
+ adopt() {
168
+ const stored = readPath(this.objectData || {}, this.content.field)
169
+ this.saved = stored === undefined || stored === null ? '' : String(stored)
170
+ this.value = this.saved
171
+ },
172
+
173
+ isSelected(option) {
174
+ return this.value === String(option.value)
175
+ },
176
+
177
+ /**
178
+ * Choose a card and save that one field.
179
+ *
180
+ * @param {object} option The chosen option.
181
+ * @return {Promise<void>}
182
+ * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
183
+ */
184
+ async choose(option) {
185
+ if (!this.objectUrl || !this.content.field) {
186
+ this.notice = this.tr('This choice cannot be saved here.')
187
+ return
188
+ }
189
+ this.notice = ''
190
+ this.busy = true
191
+ this.value = String(option.value)
192
+ try {
193
+ const data = await patchObject(this.objectUrl, { [this.content.field]: option.value })
194
+ const written = readPath(data, this.content.field)
195
+ this.saved = String(written !== undefined && written !== null ? written : option.value)
196
+ this.value = this.saved
197
+ /**
198
+ * @event saved The choice was written.
199
+ * @type {{field: string, value: unknown}}
200
+ */
201
+ this.$emit('saved', { field: this.content.field, value: option.value })
202
+ } catch (e) {
203
+ this.value = this.saved
204
+ this.notice = serverErrorMessage(e)
205
+ } finally {
206
+ this.busy = false
207
+ }
208
+ },
209
+ },
210
+ }
211
+ </script>
212
+
213
+ <style scoped>
214
+ .cn-radio-cards {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 8px;
218
+ min-width: 0;
219
+ margin: 0;
220
+ padding: 0;
221
+ border: 0;
222
+ }
223
+
224
+ .cn-radio-cards__legend {
225
+ padding: 0;
226
+ margin-bottom: 6px;
227
+ font-weight: 600;
228
+ }
229
+
230
+ .cn-radio-cards__grid {
231
+ display: grid;
232
+ gap: 10px;
233
+ grid-template-columns: minmax(0, 1fr);
234
+ }
235
+
236
+ .cn-radio-cards__grid--2 {
237
+ grid-template-columns: repeat(2, minmax(0, 1fr));
238
+ }
239
+
240
+ .cn-radio-cards__grid--3 {
241
+ grid-template-columns: repeat(3, minmax(0, 1fr));
242
+ }
243
+
244
+ .cn-radio-cards__card {
245
+ position: relative;
246
+ display: flex;
247
+ align-items: flex-start;
248
+ gap: 10px;
249
+ padding: 12px 14px;
250
+ border: 1px solid var(--color-border-maxcontrast);
251
+ border-radius: var(--cn-board-control-radius, 8px);
252
+ background: var(--color-main-background);
253
+ cursor: pointer;
254
+ }
255
+
256
+ .cn-radio-cards__card--selected {
257
+ border-color: var(--color-primary-element);
258
+ background: var(--color-primary-element-light);
259
+ box-shadow: 0 0 0 1px var(--color-primary-element);
260
+ }
261
+
262
+ .cn-radio-cards__card:focus-within {
263
+ outline: 2px solid var(--color-primary-element);
264
+ outline-offset: 2px;
265
+ }
266
+
267
+ /* The real input stays in the DOM and in the accessibility tree; the dot is its picture. */
268
+ .cn-radio-cards__input {
269
+ position: absolute;
270
+ opacity: 0;
271
+ inset: 0;
272
+ margin: 0;
273
+ cursor: pointer;
274
+ }
275
+
276
+ .cn-radio-cards__dot {
277
+ flex: 0 0 auto;
278
+ width: 14px;
279
+ height: 14px;
280
+ margin-top: 3px;
281
+ box-sizing: border-box;
282
+ border: 1px solid var(--color-border-maxcontrast);
283
+ border-radius: 50%;
284
+ background: var(--color-main-background);
285
+ }
286
+
287
+ .cn-radio-cards__card--selected .cn-radio-cards__dot {
288
+ border: 4px solid var(--color-primary-element);
289
+ }
290
+
291
+ .cn-radio-cards__text {
292
+ display: flex;
293
+ flex-direction: column;
294
+ gap: 2px;
295
+ min-width: 0;
296
+ }
297
+
298
+ .cn-radio-cards__title {
299
+ font-weight: 600;
300
+ }
301
+
302
+ .cn-radio-cards__description {
303
+ font-size: 12px;
304
+ color: var(--color-text-maxcontrast);
305
+ }
306
+
307
+ @media (max-width: 640px) {
308
+ .cn-radio-cards__grid--2,
309
+ .cn-radio-cards__grid--3 {
310
+ grid-template-columns: minmax(0, 1fr);
311
+ }
312
+ }
313
+ </style>
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnRadioCardsWidget — self-registers the `radio-cards` widget type (one
6
+ * choice out of a few cards with a title and a description, bound to an enum
7
+ * field of the current object and saved on selection) into the shared
8
+ * dashboardWidgetRegistry at module load.
9
+ */
10
+
11
+ import CnRadioCardsWidget from './CnRadioCardsWidget.vue'
12
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
13
+
14
+ registerDashboardWidget('radio-cards', {
15
+ renderer: CnRadioCardsWidget,
16
+ form: null,
17
+ defaultContent: {
18
+ field: '',
19
+ options: [],
20
+ },
21
+ displayName: 'Radio cards',
22
+ icon: 'RadioboxMarked',
23
+ surfaces: ['detail-page'],
24
+ })
25
+
26
+ export { CnRadioCardsWidget }
27
+ export default CnRadioCardsWidget
@@ -30,6 +30,10 @@
30
30
  class="cn-record-list__row"
31
31
  :class="{ 'cn-record-list__row--link': item.tag !== 'div' }"
32
32
  v-bind="item.attrs">
33
+ <span v-if="item.initials"
34
+ class="cn-record-list__avatar"
35
+ aria-hidden="true"
36
+ data-testid="cn-record-list-avatar">{{ item.initials }}</span>
33
37
  <span class="cn-record-list__main">
34
38
  <span v-if="item.title" class="cn-record-list__title">{{ item.title }}</span>
35
39
  <span v-if="item.meta" class="cn-record-list__meta">{{ item.meta }}</span>
@@ -81,6 +85,8 @@ import CnIcon from '../CnIcon/CnIcon.vue'
81
85
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
82
86
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
83
87
  import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
88
+ import { initialsOf } from '../../utils/initials.js'
89
+ import { pageFilter, pageSort } from '../../utils/pageControls.js'
84
90
  import { readPath } from '../../utils/readPath.js'
85
91
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
86
92
 
@@ -108,7 +114,8 @@ function unwrap(value) {
108
114
  * objects filtered on a reference to this record. The source keys are those of
109
115
  * `object-list` (`register`, `schema`, `filter` with the `@objectId` /
110
116
  * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
111
- * `rowRoute`); what differs is the drawing: `presentation` is `rows`, `feed`
117
+ * `rowRoute`, `pageControls`); `avatar` puts a circle with the initials of a name
118
+ * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
112
119
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
113
120
  * `"Termijn {deadline|date}"`).
114
121
  *
@@ -188,8 +195,8 @@ export default {
188
195
 
189
196
  /** @return {object} The filter with its tokens resolved. */
190
197
  resolvedFilter() {
191
- const filter = this.content?.filter
192
- return dropOptionalUnresolved(resolveFilterTokens(filter && typeof filter === 'object' ? filter : {}, this.tokenCtx))
198
+ const filter = pageFilter(this.content?.pageControls, this.tokenCtx.workspace, this.content?.filter)
199
+ return dropOptionalUnresolved(resolveFilterTokens(filter, this.tokenCtx))
193
200
  },
194
201
 
195
202
  /** @return {boolean} A required token (the record id, say) has no value yet. */
@@ -197,6 +204,11 @@ export default {
197
204
  return hasUnresolvedTokens(this.resolvedFilter)
198
205
  },
199
206
 
207
+ /** @return {object|null} The sort: the page's sort control when the widget listens to one, else its own. */
208
+ effectiveSort() {
209
+ return pageSort(this.content?.pageControls, this.tokenCtx.workspace, this.content?.sort)
210
+ },
211
+
200
212
  /** @return {number} The fetch cap. */
201
213
  limit() {
202
214
  const value = Number(this.content?.limit)
@@ -206,7 +218,7 @@ export default {
206
218
  /** @return {string} What a fetch depends on; a change fetches again. */
207
219
  requestKey() {
208
220
  const c = this.content || {}
209
- return JSON.stringify([c.register, c.schema, this.resolvedFilter, c.sort, this.limit, c.extend, this.waiting])
221
+ return JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, c.extend, this.waiting])
210
222
  },
211
223
 
212
224
  /** @return {Array<object>} The rows ready to draw. */
@@ -218,6 +230,7 @@ export default {
218
230
  key: (row && (row.id || row['@self']?.id)) || `row-${index}`,
219
231
  tag: link ? 'router-link' : 'div',
220
232
  attrs: link ? { to: link } : {},
233
+ initials: this.initialsFor(row),
221
234
  title: this.fill(c.title, row),
222
235
  meta: this.fill(c.meta, row),
223
236
  text: this.fill(c.text, row),
@@ -299,6 +312,20 @@ export default {
299
312
  return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
300
313
  },
301
314
 
315
+ /**
316
+ * The initials of the row's avatar (`avatar`: a name template such as
317
+ * `"{client.name}"`, or `{ text }`), or '' without one.
318
+ *
319
+ * @param {object} row The row.
320
+ * @return {string} One or two letters.
321
+ * @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar
322
+ */
323
+ initialsFor(row) {
324
+ const avatar = this.content?.avatar
325
+ const template = typeof avatar === 'string' ? avatar : (avatar && typeof avatar === 'object' ? avatar.text : '')
326
+ return initialsOf(this.fill(template, row))
327
+ },
328
+
302
329
  /**
303
330
  * Fill a template from a row, after translating it.
304
331
  *
@@ -422,8 +449,9 @@ export default {
422
449
  ])
423
450
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })
424
451
  const params = { _limit: this.limit }
425
- if (c.sort && typeof c.sort === 'object' && c.sort.field) {
426
- params[`_order[${c.sort.field}]`] = c.sort.dir === 'desc' ? 'desc' : 'asc'
452
+ const sort = this.effectiveSort
453
+ if (sort && typeof sort === 'object' && sort.field) {
454
+ params[`_order[${sort.field}]`] = sort.dir === 'desc' ? 'desc' : 'asc'
427
455
  }
428
456
  if (Array.isArray(c.extend)) {
429
457
  const extend = c.extend.filter((e) => typeof e === 'string' && e !== '')
@@ -497,6 +525,20 @@ export default {
497
525
  text-decoration: underline;
498
526
  }
499
527
 
528
+ .cn-record-list__avatar {
529
+ align-items: center;
530
+ background: var(--color-background-hover);
531
+ border-radius: 50%;
532
+ color: var(--color-main-text);
533
+ display: flex;
534
+ flex: none;
535
+ font-size: 12px;
536
+ font-weight: 700;
537
+ height: 36px;
538
+ justify-content: center;
539
+ width: 36px;
540
+ }
541
+
500
542
  .cn-record-list__main {
501
543
  display: flex;
502
544
  flex: 1 1 auto;
@@ -600,4 +642,11 @@ a.cn-record-list__inline-title {
600
642
  .cn-detail-page__side .cn-record-list__chevron {
601
643
  display: none;
602
644
  }
645
+
646
+ /* A row is a touch target of at least 44px on a phone. */
647
+ @media (max-width: 640px) {
648
+ .cn-record-list__row {
649
+ min-height: 44px;
650
+ }
651
+ }
603
652
  </style>