@conduction/nextcloud-vue 2.59.0 → 2.60.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 (235) 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/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
  97. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  98. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  99. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
  100. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
  101. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
  102. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
  103. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
  104. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
  105. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
  106. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
  107. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
  108. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
  109. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
  110. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
  111. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
  112. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
  113. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
  114. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
  115. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
  116. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
  117. package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
  118. package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
  119. package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
  120. package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
  121. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  122. package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
  123. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  124. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
  125. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  126. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
  127. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  128. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
  129. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
  130. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
  131. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
  132. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
  133. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
  134. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
  135. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
  136. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
  137. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
  138. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
  139. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
  140. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
  141. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  142. package/dist/esm/index.js +10 -0
  143. package/dist/esm/index.js.map +1 -1
  144. package/dist/esm/l10n/en.json.js +54 -1
  145. package/dist/esm/l10n/en.json.js.map +1 -1
  146. package/dist/esm/l10n/nl.json.js +54 -1
  147. package/dist/esm/l10n/nl.json.js.map +1 -1
  148. package/dist/esm/nextcloud-vue.css +931 -4
  149. package/dist/esm/utils/dateVariant.js +124 -0
  150. package/dist/esm/utils/dateVariant.js.map +1 -0
  151. package/dist/esm/utils/detailActionModel.js +260 -0
  152. package/dist/esm/utils/detailActionModel.js.map +1 -0
  153. package/dist/esm/utils/dueRule.js +84 -0
  154. package/dist/esm/utils/dueRule.js.map +1 -0
  155. package/dist/esm/utils/fetchAggregate.js +37 -1
  156. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  157. package/dist/esm/utils/fetchFilterCounts.js +186 -0
  158. package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
  159. package/dist/esm/utils/libraryWidgetKeys.js +5 -0
  160. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  161. package/dist/esm/utils/readPath.js +30 -0
  162. package/dist/esm/utils/readPath.js.map +1 -0
  163. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  164. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  165. package/dist/nextcloud-vue.cjs.js +14970 -8991
  166. package/dist/nextcloud-vue.cjs.js.map +2 -2
  167. package/dist/nextcloud-vue.css +931 -4
  168. package/l10n/en.json +54 -1
  169. package/l10n/nl.json +54 -1
  170. package/package.json +1 -1
  171. package/scripts/.jsdoc-baselines.json +3 -0
  172. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  173. package/src/components/CnAppNav/CnAppNav.md +26 -0
  174. package/src/components/CnAppNav/CnAppNav.vue +104 -1
  175. package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
  176. package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
  177. package/src/components/CnBoardView/CnBoardView.vue +77 -0
  178. package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
  179. package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
  180. package/src/components/CnBrandStripe/index.js +4 -0
  181. package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
  182. package/src/components/CnConversationThread/CnConversationThread.md +51 -0
  183. package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
  184. package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
  185. package/src/components/CnConversationThread/index.js +4 -0
  186. package/src/components/CnDetailPage/CnDetailPage.md +16 -0
  187. package/src/components/CnDetailPage/CnDetailPage.vue +683 -48
  188. package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
  189. package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
  190. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
  191. package/src/components/CnDocumentReviewList/index.js +4 -0
  192. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
  193. package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
  194. package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
  195. package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
  196. package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
  197. package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
  198. package/src/components/CnNextStepCard/index.js +4 -0
  199. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
  200. package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
  201. package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
  202. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
  203. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
  204. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
  205. package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
  206. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
  207. package/src/components/CnSegmentedControl/index.js +4 -0
  208. package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
  209. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
  210. package/src/components/CnStackedBarWidget/index.js +14 -0
  211. package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
  212. package/src/components/CnStackedBarWidgetForm/index.js +4 -0
  213. package/src/components/CnTabs/CnTab.vue +35 -0
  214. package/src/components/CnTabs/CnTabs.vue +244 -10
  215. package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
  216. package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
  217. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
  218. package/src/components/CnWeekStripWidget/index.js +14 -0
  219. package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
  220. package/src/components/CnWeekStripWidgetForm/index.js +4 -0
  221. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
  222. package/src/components/index.js +11 -0
  223. package/src/css/detail-page.css +47 -0
  224. package/src/index.js +9 -0
  225. package/src/schemas/app-manifest-v2.schema.json +389 -1
  226. package/src/schemas/app-manifest.schema.json +2 -1
  227. package/src/utils/dateVariant.js +120 -0
  228. package/src/utils/detailActionModel.js +256 -0
  229. package/src/utils/dueRule.js +94 -0
  230. package/src/utils/fetchAggregate.js +36 -0
  231. package/src/utils/fetchFilterCounts.js +182 -0
  232. package/src/utils/index.js +1 -0
  233. package/src/utils/libraryWidgetKeys.js +5 -0
  234. package/src/utils/readPath.js +27 -0
  235. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -4,7 +4,39 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <div v-if="visible" class="cn-banner-widget">
7
+ <section
8
+ v-if="visible && isAttention"
9
+ class="cn-banner-widget cn-banner-widget--attention"
10
+ :class="'cn-banner-widget--' + resolvedVariant"
11
+ :aria-labelledby="headingId"
12
+ data-testid="cn-banner-widget-attention">
13
+ <div class="cn-banner-widget__body">
14
+ <p v-if="resolvedKicker" class="cn-banner-widget__kicker">
15
+ {{ resolvedKicker }}
16
+ </p>
17
+ <h3 :id="headingId" class="cn-banner-widget__title">
18
+ {{ displayTitle }}
19
+ </h3>
20
+ <p v-if="displayReason" class="cn-banner-widget__reason">
21
+ {{ displayReason }}
22
+ </p>
23
+ </div>
24
+ <div v-if="resolvedActions.length > 0" class="cn-banner-widget__actions">
25
+ <component
26
+ :is="action.href ? 'a' : 'button'"
27
+ v-for="action in resolvedActions"
28
+ :key="action.key"
29
+ class="cn-banner-widget__action"
30
+ :class="action.primary ? 'cn-banner-widget__action--primary' : 'cn-banner-widget__action--secondary'"
31
+ :href="action.href || null"
32
+ :type="action.href ? null : 'button'"
33
+ data-testid="cn-banner-widget-action"
34
+ @click="onActionClick($event, action)">
35
+ {{ action.label }}
36
+ </component>
37
+ </div>
38
+ </section>
39
+ <div v-else-if="visible" class="cn-banner-widget">
8
40
  <NcNoteCard :type="resolvedVariant" class="cn-banner-widget__card">
9
41
  <component
10
42
  :is="clickable ? 'a' : 'span'"
@@ -22,11 +54,19 @@
22
54
  <script>
23
55
  import { NcNoteCard } from '@nextcloud/vue'
24
56
  import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
57
+ import { safeHref } from '../../utils/safeHref.js'
58
+ import { nextUid } from '../../utils/uid.js'
25
59
  import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
26
60
 
27
61
  /** Variants understood by NcNoteCard. */
28
62
  const VARIANTS = ['info', 'warning', 'error', 'success']
29
63
 
64
+ /** Layouts: the note card (default) or the attention card. */
65
+ const LAYOUTS = ['banner', 'attention']
66
+
67
+ /** An attention card offers a primary and a secondary action, no more. */
68
+ const MAX_ACTIONS = 2
69
+
30
70
  /**
31
71
  * CnBannerWidget — declarative notice banner for dashboards and v2 pages
32
72
  * (`banner` widget type, Wave 1 of nextcloud-vue#91).
@@ -56,6 +96,29 @@ const VARIANTS = ['info', 'warning', 'error', 'success']
56
96
  * Fail-safe: with a `visibleWhen`, the banner stays HIDDEN until the
57
97
  * condition evaluates true — a failed fetch never breaks (or spams) a
58
98
  * dashboard.
99
+ *
100
+ * `layout: "attention"` turns the banner into an attention card: a card with
101
+ * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line
102
+ * and up to two `actions` (the first is the primary one unless an action says
103
+ * `primary` itself). `visibleWhen` works the same way. Without `layout` the
104
+ * note card above renders, as it always did.
105
+ *
106
+ * ```json
107
+ * {
108
+ * "widgetKey": "banner",
109
+ * "props": {
110
+ * "layout": "attention",
111
+ * "variant": "error",
112
+ * "kicker": "First today",
113
+ * "title": "Parking permits city centre",
114
+ * "reason": "The deadline ends today.",
115
+ * "actions": [
116
+ * { "label": "Open case", "route": { "name": "CaseDetail", "params": { "id": "2026-0061" } } },
117
+ * { "label": "Suspend deadline", "route": "CaseSuspend" }
118
+ * ]
119
+ * }
120
+ * }
121
+ * ```
59
122
  */
60
123
  export default {
61
124
  name: 'CnBannerWidget',
@@ -111,10 +174,57 @@ export default {
111
174
  default: null,
112
175
  },
113
176
 
177
+ /**
178
+ * How the banner renders: `banner` (the note card, default) or
179
+ * `attention` (a card with a severity edge, kicker, title, reason and
180
+ * up to two actions). Empty falls back to the `content` blob, then
181
+ * `banner`.
182
+ *
183
+ * @type {''|'banner'|'attention'}
184
+ */
185
+ layout: {
186
+ type: String,
187
+ default: '',
188
+ validator: (v) => v === '' || LAYOUTS.includes(v),
189
+ },
190
+
191
+ /** Attention card: the small label above the title ("First today"). */
192
+ kicker: {
193
+ type: String,
194
+ default: '',
195
+ },
196
+
197
+ /** Attention card: the title. Falls back to `text` when empty. */
198
+ title: {
199
+ type: String,
200
+ default: '',
201
+ },
202
+
203
+ /** Attention card: the line under the title that says why this needs attention. `{value}` is replaced like in `text`. */
204
+ reason: {
205
+ type: String,
206
+ default: '',
207
+ },
208
+
209
+ /**
210
+ * Attention card: at most two actions. Each is `{ label, route?,
211
+ * href?, primary?, id? }`: `route` is a route name or location,
212
+ * `href` an URL. An action with neither renders a button that emits
213
+ * `action`. The first action is the primary one unless one sets
214
+ * `primary: true`.
215
+ *
216
+ * @type {Array<{label: string, route?: (string|object), href?: string, primary?: boolean, id?: string}>|null}
217
+ */
218
+ actions: {
219
+ type: Array,
220
+ default: null,
221
+ },
222
+
114
223
  /**
115
224
  * Stored content blob (CnDashboardPage registry branch) carrying the
116
- * same keys as the flat props: `{ variant, text, visibleWhen, route }`.
117
- * Explicit flat props win on collision.
225
+ * same keys as the flat props: `{ variant, text, visibleWhen, route,
226
+ * layout, kicker, title, reason, actions }`. Explicit flat props win
227
+ * on collision.
118
228
  */
119
229
  content: {
120
230
  type: Object,
@@ -138,8 +248,21 @@ export default {
138
248
  },
139
249
  },
140
250
 
251
+ emits: [
252
+ /**
253
+ * Emitted when an attention-card action without a `route` or `href`
254
+ * is clicked, so the host can run it.
255
+ *
256
+ * @event action
257
+ * @type {{id: (string|undefined), label: string, index: number}}
258
+ */
259
+ 'action',
260
+ ],
261
+
141
262
  data() {
142
263
  return {
264
+ /** Id that ties the attention card's region to its title. */
265
+ headingId: `cn-banner-title-${nextUid()}`,
143
266
  /** Evaluated visibleWhen outcome (null = not yet evaluated). */
144
267
  conditionMet: null,
145
268
  /**
@@ -179,13 +302,11 @@ export default {
179
302
  * the text renders as written — a `{value}` placeholder only makes
180
303
  * sense on a conditional banner, which stays hidden until its
181
304
  * predicate (and therefore its value) resolves.
305
+ *
306
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
182
307
  */
183
308
  displayText() {
184
- const text = this.resolvedText
185
- if (this.conditionValue === null || !text.includes('{value}')) {
186
- return text
187
- }
188
- return text.replaceAll('{value}', String(this.conditionValue))
309
+ return this.fillValue(this.resolvedText)
189
310
  },
190
311
 
191
312
  /** The effective click-through route (prop → content → null). */
@@ -193,9 +314,101 @@ export default {
193
314
  return this.route || (this.content && this.content.route) || null
194
315
  },
195
316
 
196
- /** Whether the banner renders: no condition = always; else the evaluated outcome. */
317
+ /**
318
+ * Whether the attention card renders instead of the note card.
319
+ *
320
+ * @return {boolean}
321
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
322
+ */
323
+ isAttention() {
324
+ return (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'
325
+ },
326
+
327
+ /**
328
+ * The attention card's kicker (prop, else content).
329
+ *
330
+ * @return {string}
331
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
332
+ */
333
+ resolvedKicker() {
334
+ const kicker = this.kicker || (this.content && this.content.kicker) || ''
335
+ return kicker
336
+ },
337
+
338
+ /**
339
+ * The attention card's title: `title`, else `text`, with `{value}` filled in.
340
+ *
341
+ * @return {string}
342
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
343
+ */
344
+ displayTitle() {
345
+ const title = this.title || (this.content && this.content.title) || ''
346
+ return title ? this.fillValue(title) : this.displayText
347
+ },
348
+
349
+ /**
350
+ * The attention card's reason line. When the title already used
351
+ * `text`, the text is not repeated here.
352
+ *
353
+ * @return {string}
354
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
355
+ */
356
+ displayReason() {
357
+ const reason = this.reason || (this.content && this.content.reason) || ''
358
+ if (reason) {
359
+ return this.fillValue(reason)
360
+ }
361
+ const title = this.title || (this.content && this.content.title) || ''
362
+ return title ? this.displayText : ''
363
+ },
364
+
365
+ /**
366
+ * The attention card's actions: the first two that have a label, each
367
+ * with its href and whether it is the primary one.
368
+ *
369
+ * @return {Array<{key: string, id: (string|undefined), label: string, href: string, target: (object|null), primary: boolean, index: number}>}
370
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
371
+ */
372
+ resolvedActions() {
373
+ const raw = this.actions || (this.content && this.content.actions) || []
374
+ if (!Array.isArray(raw)) {
375
+ return []
376
+ }
377
+ const usable = raw
378
+ .filter((action) => action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '')
379
+ .slice(0, MAX_ACTIONS)
380
+ const declaredPrimary = usable.findIndex((action) => action.primary === true)
381
+ const primaryIndex = declaredPrimary === -1 ? 0 : declaredPrimary
382
+ return usable.map((action, index) => {
383
+ let target = null
384
+ let href = ''
385
+ if (action.route) {
386
+ target = typeof action.route === 'string' ? { name: action.route } : action.route
387
+ href = resolveHref(target, this.$router)
388
+ } else if (typeof action.href === 'string' && action.href !== '') {
389
+ const safe = safeHref(action.href)
390
+ href = safe === '#' ? '' : safe
391
+ }
392
+ return {
393
+ key: `${index}-${action.id || action.label}`,
394
+ id: action.id,
395
+ label: action.label,
396
+ href,
397
+ target,
398
+ primary: index === primaryIndex,
399
+ index,
400
+ }
401
+ })
402
+ },
403
+
404
+ /**
405
+ * Whether the banner renders: no condition = always; else the evaluated outcome.
406
+ *
407
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
408
+ */
197
409
  visible() {
198
- if (this.resolvedText === '') {
410
+ const title = this.title || (this.content && this.content.title) || ''
411
+ if (this.resolvedText === '' && !(this.isAttention && title !== '')) {
199
412
  return false
200
413
  }
201
414
  if (!this.resolvedVisibleWhen) {
@@ -235,6 +448,40 @@ export default {
235
448
  },
236
449
 
237
450
  methods: {
451
+ /**
452
+ * Replace `{value}` in a text by the value the predicate read, once
453
+ * one is known.
454
+ *
455
+ * @param {string} text The text.
456
+ * @return {string} The text with the value filled in.
457
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
458
+ */
459
+ fillValue(text) {
460
+ if (this.conditionValue === null || !text.includes('{value}')) {
461
+ return text
462
+ }
463
+ return text.replaceAll('{value}', String(this.conditionValue))
464
+ },
465
+
466
+ /**
467
+ * Run an attention-card action: route an in-app link, leave an
468
+ * external link to the browser, and emit `action` for a button.
469
+ *
470
+ * @param {MouseEvent} event The click.
471
+ * @param {object} action The resolved action.
472
+ * @return {void}
473
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
474
+ */
475
+ onActionClick(event, action) {
476
+ if (action.target) {
477
+ followLinkClick(event, action.target, this.$router)
478
+ return
479
+ }
480
+ if (!action.href) {
481
+ this.$emit('action', { id: action.id, label: action.label, index: action.index })
482
+ }
483
+ },
484
+
238
485
  /**
239
486
  * Resolve the `visibleWhen` verdict. A host-injected
240
487
  * `conditionOutcome` wins outright — the host already ran the
@@ -302,4 +549,112 @@ export default {
302
549
  text-decoration: underline;
303
550
  color: inherit;
304
551
  }
552
+
553
+ /* Attention card. The severity edge is an inset shadow, so it takes no width. */
554
+ .cn-banner-widget--attention {
555
+ --cn-banner-accent: var(--color-primary-element);
556
+ --cn-banner-accent-text: var(--color-primary-element);
557
+ display: flex;
558
+ flex-wrap: wrap;
559
+ align-items: center;
560
+ gap: 16px 24px;
561
+ box-sizing: border-box;
562
+ padding: 20px 24px 20px 28px;
563
+ border: 1px solid var(--color-border);
564
+ border-radius: var(--border-radius-container, var(--border-radius-large, 12px));
565
+ background: var(--color-main-background);
566
+ box-shadow: inset 4px 0 0 0 var(--cn-banner-accent);
567
+ }
568
+
569
+ .cn-banner-widget--attention.cn-banner-widget--warning {
570
+ --cn-banner-accent: var(--color-element-warning, var(--color-warning));
571
+ --cn-banner-accent-text: var(--color-text-warning, var(--color-warning-text, var(--color-main-text)));
572
+ }
573
+
574
+ .cn-banner-widget--attention.cn-banner-widget--error {
575
+ --cn-banner-accent: var(--color-element-error, var(--color-error));
576
+ --cn-banner-accent-text: var(--color-text-error, var(--color-error-text));
577
+ }
578
+
579
+ .cn-banner-widget--attention.cn-banner-widget--success {
580
+ --cn-banner-accent: var(--color-element-success, var(--color-success));
581
+ --cn-banner-accent-text: var(--color-text-success, var(--color-success-text));
582
+ }
583
+
584
+ .cn-banner-widget__body {
585
+ display: flex;
586
+ flex: 1 1 320px;
587
+ flex-direction: column;
588
+ gap: 6px;
589
+ min-width: 0;
590
+ }
591
+
592
+ .cn-banner-widget__kicker {
593
+ margin: 0;
594
+ font-size: 0.85em;
595
+ font-weight: 700;
596
+ letter-spacing: 0.06em;
597
+ text-transform: uppercase;
598
+ color: var(--cn-banner-accent-text);
599
+ }
600
+
601
+ .cn-banner-widget__title {
602
+ margin: 0;
603
+ font-size: 1.3em;
604
+ font-weight: 700;
605
+ line-height: 1.3;
606
+ color: var(--color-main-text);
607
+ }
608
+
609
+ .cn-banner-widget__reason {
610
+ margin: 0;
611
+ color: var(--color-text-maxcontrast);
612
+ }
613
+
614
+ .cn-banner-widget__actions {
615
+ display: flex;
616
+ flex-wrap: wrap;
617
+ gap: 10px;
618
+ }
619
+
620
+ .cn-banner-widget__action {
621
+ display: inline-flex;
622
+ align-items: center;
623
+ justify-content: center;
624
+ box-sizing: border-box;
625
+ min-height: var(--default-clickable-area, 40px);
626
+ margin: 0;
627
+ padding: 0 16px;
628
+ border: 1px solid var(--color-border-maxcontrast);
629
+ border-radius: var(--border-radius-element, var(--border-radius-large, 8px));
630
+ font: inherit;
631
+ font-weight: 600;
632
+ text-decoration: none;
633
+ cursor: pointer;
634
+ }
635
+
636
+ .cn-banner-widget__action--secondary {
637
+ background: var(--color-main-background);
638
+ color: var(--color-main-text);
639
+ }
640
+
641
+ .cn-banner-widget__action--secondary:hover {
642
+ background: var(--color-background-hover);
643
+ }
644
+
645
+ .cn-banner-widget__action--primary {
646
+ border-color: var(--color-primary-element);
647
+ background: var(--color-primary-element);
648
+ color: var(--color-primary-element-text);
649
+ }
650
+
651
+ .cn-banner-widget__action--primary:hover {
652
+ border-color: var(--color-primary-element-hover);
653
+ background: var(--color-primary-element-hover);
654
+ }
655
+
656
+ .cn-banner-widget__action:focus-visible {
657
+ outline: 2px solid var(--color-main-text);
658
+ outline-offset: 2px;
659
+ }
305
660
  </style>
@@ -5,6 +5,26 @@
5
5
 
6
6
  <template>
7
7
  <div class="cn-banner-form">
8
+ <NcSelect
9
+ :modelValue="layout"
10
+ :options="layoutOptions"
11
+ :inputLabel="t('nextcloud-vue', 'Layout')"
12
+ :clearable="false"
13
+ @update:modelValue="update('layout', $event)" />
14
+ <template v-if="layout === 'attention'">
15
+ <NcTextField
16
+ :modelValue="kicker"
17
+ :label="t('nextcloud-vue', 'Label above the title')"
18
+ @update:modelValue="update('kicker', $event)" />
19
+ <NcTextField
20
+ :modelValue="title"
21
+ :label="t('nextcloud-vue', 'Title')"
22
+ @update:modelValue="update('title', $event)" />
23
+ <NcTextField
24
+ :modelValue="reason"
25
+ :label="t('nextcloud-vue', 'Reason (why this needs attention)')"
26
+ @update:modelValue="update('reason', $event)" />
27
+ </template>
8
28
  <NcTextField
9
29
  :modelValue="text"
10
30
  :label="t('nextcloud-vue', 'Banner text')"
@@ -22,6 +42,9 @@
22
42
  <p class="cn-banner-form__hint">
23
43
  {{ t('nextcloud-vue', 'A conditional banner (visibleWhen) is declared in the manifest.') }}
24
44
  </p>
45
+ <p v-if="layout === 'attention'" class="cn-banner-form__hint">
46
+ {{ t('nextcloud-vue', 'The actions of an attention card are declared in the manifest.') }}
47
+ </p>
25
48
  </div>
26
49
  </template>
27
50
 
@@ -34,7 +57,9 @@ import { NcSelect, NcTextField } from '@nextcloud/vue'
34
57
  *
35
58
  * Edits the banner text, the severity variant, and the optional
36
59
  * click-through route (a page id). The `visibleWhen` condition is a
37
- * manifest-authored construct and is round-tripped untouched. Emits
60
+ * manifest-authored construct and is round-tripped untouched. With the
61
+ * `attention` layout it also edits the kicker, the title and the reason; the
62
+ * card's `actions` are manifest-authored and round-tripped too. Emits
38
63
  * `update:content` with the assembled blob on every change; `validate()`
39
64
  * requires a non-empty text. Used by both `CnAddWidgetModal` and the cog
40
65
  * `CnWidgetStyleEditorModal`.
@@ -85,6 +110,12 @@ export default {
85
110
  route: typeof initial.route === 'string' ? initial.route : '',
86
111
  // Round-tripped untouched — the form doesn't edit conditions.
87
112
  visibleWhen: initial.visibleWhen ?? null,
113
+ layout: initial.layout === 'attention' ? 'attention' : 'banner',
114
+ kicker: initial.kicker ?? '',
115
+ title: initial.title ?? '',
116
+ reason: initial.reason ?? '',
117
+ // Round-tripped untouched: actions are written in the manifest.
118
+ actions: Array.isArray(initial.actions) ? initial.actions : null,
88
119
  }
89
120
  },
90
121
 
@@ -94,14 +125,40 @@ export default {
94
125
  return ['info', 'warning', 'error']
95
126
  },
96
127
 
97
- /** The assembled content blob from the current field values. */
128
+ /**
129
+ * Selectable layouts: the note card or the attention card.
130
+ *
131
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
132
+ */
133
+ layoutOptions() {
134
+ return ['banner', 'attention']
135
+ },
136
+
137
+ /**
138
+ * The assembled content blob from the current field values. The
139
+ * attention keys are only written for an attention card, so a plain
140
+ * banner's stored content stays what it always was.
141
+ *
142
+ * @return {object}
143
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
144
+ */
98
145
  assembledContent() {
99
- return {
146
+ const content = {
100
147
  text: this.text,
101
148
  variant: this.variant,
102
149
  route: this.route || null,
103
150
  visibleWhen: this.visibleWhen,
104
151
  }
152
+ if (this.layout === 'attention') {
153
+ content.layout = 'attention'
154
+ content.kicker = this.kicker
155
+ content.title = this.title
156
+ content.reason = this.reason
157
+ if (this.actions !== null) {
158
+ content.actions = this.actions
159
+ }
160
+ }
161
+ return content
105
162
  },
106
163
  },
107
164
 
@@ -123,8 +180,13 @@ export default {
123
180
  * Validate the form; an empty array means valid.
124
181
  *
125
182
  * @return {string[]} the validation errors.
183
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
126
184
  */
127
185
  validate() {
186
+ if (this.layout === 'attention') {
187
+ const hasHeadline = (this.title && this.title.trim() !== '') || (this.text && this.text.trim() !== '')
188
+ return hasHeadline ? [] : [t('nextcloud-vue', 'An attention card needs a title')]
189
+ }
128
190
  if (!this.text || this.text.trim() === '') {
129
191
  return [t('nextcloud-vue', 'A banner text is required')]
130
192
  }
@@ -83,11 +83,19 @@
83
83
  v-for="card in column.cards"
84
84
  :key="`card-${cardKey(card)}`"
85
85
  class="cn-board-view__card"
86
+ :class="dueState(card) ? `cn-board-view__card--${dueState(card)}` : null"
86
87
  role="listitem"
87
88
  data-testid="cn-board-card"
88
89
  :data-card-id="cardKey(card)"
89
90
  :draggable="canMove"
90
91
  @dragstart="onDragStart(card, column, $event)">
92
+ <span
93
+ v-if="dueLabel(card)"
94
+ class="cn-board-view__due"
95
+ :class="`cn-board-view__due--${dueState(card)}`"
96
+ data-testid="cn-board-due">
97
+ {{ dueLabel(card) }}
98
+ </span>
91
99
  <span
92
100
  v-for="field in cardFields"
93
101
  :key="`f-${cardKey(card)}-${field}`"
@@ -162,6 +170,7 @@ import { translate as t } from '@nextcloud/l10n'
162
170
  import { buildBoardColumns } from '../../utils/boardColumns.js'
163
171
  import { buildSwimlanes } from '../../utils/boardSwimlanes.js'
164
172
  import { DROP_OUTCOMES, runBoardDrop } from '../../utils/boardTransition.js'
173
+ import { dueStateForRow } from '../../utils/dueRule.js'
165
174
  import { isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
166
175
 
167
176
  /**
@@ -252,6 +261,22 @@ export default {
252
261
  type: Boolean,
253
262
  default: false,
254
263
  },
264
+
265
+ /**
266
+ * Marks late cards: `{ field, soonDays? }`. `field` is the dot-path to
267
+ * a card's due date. A date before today gets an error edge and the
268
+ * label "Overdue"; a date within `soonDays` (default 3) gets "Due
269
+ * soon" in the warning colour. `null` (the default) marks nothing.
270
+ *
271
+ * The same rule shape the table's date cell uses, so a list and its
272
+ * board agree on what late means.
273
+ *
274
+ * @type {{field: string, soonDays?: number}|null}
275
+ */
276
+ dueRule: {
277
+ type: Object,
278
+ default: null,
279
+ },
255
280
  },
256
281
 
257
282
  emits: ['card-click', 'card-aux-click', 'moved', 'refused', 'stale'],
@@ -264,6 +289,8 @@ export default {
264
289
  refusal: '',
265
290
  /** The card being dragged, and where from. */
266
291
  dragging: null,
292
+ /** The day late marking measures from. */
293
+ today: new Date(),
267
294
  }
268
295
  },
269
296
 
@@ -336,6 +363,33 @@ export default {
336
363
 
337
364
  preventMiddleClickAutoscroll,
338
365
 
366
+ /**
367
+ * A card's due state under `dueRule`.
368
+ *
369
+ * @param {object} card The card's row.
370
+ * @return {'overdue'|'soon'|'ok'|null} The state, or null when no rule applies.
371
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards
372
+ */
373
+ dueState(card) {
374
+ return dueStateForRow(card, this.dueRule, this.today)
375
+ },
376
+
377
+ /**
378
+ * The words a late card carries. Colour alone would not reach a reader
379
+ * who cannot see it, or cannot tell red from amber.
380
+ *
381
+ * @param {object} card The card's row.
382
+ * @return {string} "Overdue", "Due soon", or '' for a card that is fine.
383
+ * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards
384
+ */
385
+ dueLabel(card) {
386
+ const state = this.dueState(card)
387
+ if (state === 'overdue') {
388
+ return t('nextcloud-vue', 'Overdue')
389
+ }
390
+ return state === 'soon' ? t('nextcloud-vue', 'Due soon') : ''
391
+ },
392
+
339
393
  /**
340
394
  * Open one card. The pointer and the keyboard both come through here,
341
395
  * so a reader on Enter reaches the same record as a reader on click.
@@ -561,6 +615,29 @@ export default {
561
615
  display: block;
562
616
  }
563
617
 
618
+ /* A late card: an edge in the error colour, drawn as an inset shadow so the
619
+ card keeps its size and its rounded corners. The label below carries the
620
+ meaning; the edge only helps a sighted reader find it. */
621
+ .cn-board-view__card--overdue {
622
+ border-color: var(--color-error);
623
+ box-shadow: inset 3px 0 0 var(--color-error);
624
+ }
625
+
626
+ .cn-board-view__due {
627
+ display: block;
628
+ font-size: 0.85em;
629
+ font-weight: 700;
630
+ }
631
+
632
+ .cn-board-view__due--overdue {
633
+ color: var(--color-error-text, var(--color-error));
634
+ }
635
+
636
+ .cn-board-view__due--soon {
637
+ color: var(--color-warning-text, var(--color-main-text));
638
+ font-weight: 600;
639
+ }
640
+
564
641
  /*
565
642
  * The card's opening control. The cursor and the focus ring live here now,
566
643
  * on the thing that actually does something, rather than on the box.