@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
@@ -240,6 +240,29 @@
240
240
  "$ref": "#/$defs/primaryAction",
241
241
  "description": "App-wide default primary action rendered as an NcAppNavigationNew button above the main menu list. A page-scoped pages[].primaryAction for the current route wins over this default."
242
242
  },
243
+ "brand": {
244
+ "type": "object",
245
+ "additionalProperties": false,
246
+ "description": "A brand block at the very top of the app navigation, above the primary action: a logo, a name and a caption. Rendered by CnAppNav. Omitted, nothing renders there.",
247
+ "properties": {
248
+ "logo": {
249
+ "type": "string",
250
+ "description": "URL of the logo image. Decorative when `name` is set."
251
+ },
252
+ "name": {
253
+ "type": "string",
254
+ "description": "i18n key / text for the app or organisation name."
255
+ },
256
+ "caption": {
257
+ "type": "string",
258
+ "description": "i18n key / text for the line under the name, for example the organisation."
259
+ },
260
+ "alt": {
261
+ "type": "string",
262
+ "description": "i18n key / alternative text for the logo. Used only when no `name` is set."
263
+ }
264
+ }
265
+ },
243
266
  "featureRequestRepo": {
244
267
  "type": "string",
245
268
  "description": "`<owner>/<repo>` slug on the forge that the in-product feature-request deep-link targets (provided to descendants as `cnFeatureRequestRepo`). Falls back to `Conduction/<appId>` when omitted."
@@ -976,6 +999,72 @@
976
999
  "swimlaneField": {
977
1000
  "type": "string",
978
1001
  "description": "A second field to group the board into rows by, while the columns stay the stages. Cards with no value for it land in one named row, last, rather than being dropped: grouping by handler and hiding the unassigned work turns a board into a picture of what is already somebody's problem."
1002
+ },
1003
+ "dueRule": {
1004
+ "$ref": "#/$defs/dueRule",
1005
+ "description": "Marks late cards. A card whose date is before today gets an error edge and the label 'Overdue'; one due within `soonDays` gets 'Due soon'. Omitted, no card is marked."
1006
+ }
1007
+ }
1008
+ },
1009
+ "headerPill": {
1010
+ "type": "object",
1011
+ "additionalProperties": false,
1012
+ "required": ["field"],
1013
+ "description": "A pill in a detail page header, rendered through CnStatusBadge from one field of the record. Nothing renders while the field is empty.",
1014
+ "properties": {
1015
+ "field": {
1016
+ "type": "string",
1017
+ "minLength": 1,
1018
+ "description": "Dot-path to the value on the record."
1019
+ },
1020
+ "colorMap": {
1021
+ "type": "object",
1022
+ "description": "Raw value to badge variant. Keys are matched ignoring case.",
1023
+ "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
1024
+ },
1025
+ "labels": {
1026
+ "type": "object",
1027
+ "description": "Raw value to the i18n key / text to show. A value without an entry is shown as is.",
1028
+ "additionalProperties": { "type": "string" }
1029
+ },
1030
+ "variant": {
1031
+ "type": "string",
1032
+ "enum": ["default", "primary", "success", "warning", "error", "info"],
1033
+ "default": "default",
1034
+ "description": "Badge variant for a value `colorMap` does not name."
1035
+ }
1036
+ }
1037
+ },
1038
+ "dueRule": {
1039
+ "type": "object",
1040
+ "additionalProperties": false,
1041
+ "required": ["field"],
1042
+ "description": "How a surface decides that a dated item is late. One shape for boards and lists, so a list and its board agree. A date before today is overdue. Days are whole calendar days in the reader's own timezone.",
1043
+ "properties": {
1044
+ "field": {
1045
+ "type": "string",
1046
+ "minLength": 1,
1047
+ "description": "Dot-path to the due date on the row."
1048
+ },
1049
+ "soonDays": {
1050
+ "type": "integer",
1051
+ "minimum": 0,
1052
+ "default": 3,
1053
+ "description": "How many days ahead still counts as due soon. 0 marks only today."
1054
+ },
1055
+ "variantWhen": {
1056
+ "type": "array",
1057
+ "description": "The full rule list, in the shape a table's date cell uses, for a board that needs more than `soonDays`. Compared against the calendar days until the date (0 is today, negative is past); first match wins. `error` marks the card overdue, `warning` due soon. When set, `soonDays` is ignored.",
1058
+ "items": {
1059
+ "type": "object",
1060
+ "additionalProperties": false,
1061
+ "required": ["op", "value", "variant"],
1062
+ "properties": {
1063
+ "op": { "type": "string", "enum": ["eq", "neq", "gt", "gte", "lt", "lte"] },
1064
+ "value": { "type": "number" },
1065
+ "variant": { "type": "string", "enum": ["default", "success", "warning", "error", "danger"] }
1066
+ }
1067
+ }
979
1068
  }
980
1069
  }
981
1070
  },
@@ -1575,6 +1664,47 @@
1575
1664
  { "type": "string" },
1576
1665
  { "type": "object", "additionalProperties": true }
1577
1666
  ]
1667
+ },
1668
+ "layout": {
1669
+ "type": "string",
1670
+ "enum": ["banner", "attention"],
1671
+ "default": "banner",
1672
+ "description": "How the banner renders. `banner` (default) is the note card. `attention` is a card with a severity edge, a kicker, a title, a reason line and up to two actions."
1673
+ },
1674
+ "kicker": {
1675
+ "type": "string",
1676
+ "description": "Attention card: the small label above the title."
1677
+ },
1678
+ "title": {
1679
+ "type": "string",
1680
+ "description": "Attention card: the title. Falls back to `text`."
1681
+ },
1682
+ "reason": {
1683
+ "type": "string",
1684
+ "description": "Attention card: the line that says why this needs attention. `{value}` is replaced by the value visibleWhen read."
1685
+ },
1686
+ "actions": {
1687
+ "type": "array",
1688
+ "maxItems": 2,
1689
+ "description": "Attention card: at most two actions. The first is the primary one unless an action sets `primary: true`.",
1690
+ "items": {
1691
+ "type": "object",
1692
+ "required": ["label"],
1693
+ "additionalProperties": false,
1694
+ "properties": {
1695
+ "id": { "type": "string", "description": "Identifier passed with the `action` event of an action that has no route or href." },
1696
+ "label": { "type": "string", "description": "The text on the action." },
1697
+ "route": {
1698
+ "description": "A vue-router route NAME (string) or a full location object.",
1699
+ "oneOf": [
1700
+ { "type": "string" },
1701
+ { "type": "object", "additionalProperties": true }
1702
+ ]
1703
+ },
1704
+ "href": { "type": "string", "description": "An URL, used when there is no route." },
1705
+ "primary": { "type": "boolean", "description": "Render this action as the primary one." }
1706
+ }
1707
+ }
1578
1708
  }
1579
1709
  }
1580
1710
  }
@@ -1875,6 +2005,125 @@
1875
2005
  }
1876
2006
  }
1877
2007
  }
2008
+ },
2009
+ {
2010
+ "description": "week-strip catalog widget (CnWeekStripWidget): the current week as day columns with the dated items under each day. Takes a `props.content` blob. Items come from an OpenRegister `source` (one request for the week, bucketed by `dateField`) or from static `items`.",
2011
+ "if": {
2012
+ "properties": { "widgetKey": { "const": "week-strip" } },
2013
+ "required": ["widgetKey"]
2014
+ },
2015
+ "then": {
2016
+ "properties": {
2017
+ "props": {
2018
+ "type": "object",
2019
+ "properties": {
2020
+ "content": {
2021
+ "type": "object",
2022
+ "additionalProperties": true,
2023
+ "properties": {
2024
+ "days": { "type": "integer", "enum": [5, 7], "default": 5, "description": "5 working days (default) or the whole week. The week starts on Monday." },
2025
+ "source": {
2026
+ "type": "object",
2027
+ "additionalProperties": false,
2028
+ "required": ["register", "schema"],
2029
+ "description": "OpenRegister object query; `filter` supports the shared @-token grammar.",
2030
+ "properties": {
2031
+ "register": { "type": "string" },
2032
+ "schema": { "type": "string" },
2033
+ "filter": { "type": "object", "additionalProperties": true },
2034
+ "limit": { "type": "integer", "minimum": 1, "description": "Cap on the records fetched for the week (default 100)." }
2035
+ }
2036
+ },
2037
+ "dateField": { "type": "string", "description": "The field the items are bucketed by. Required with a source." },
2038
+ "titleField": { "type": "string", "description": "The field shown as the item title. Defaults to the object's display name." },
2039
+ "metaFields": { "type": "array", "items": { "type": "string" }, "description": "Fields shown under the title." },
2040
+ "itemRoute": { "type": "string", "description": "Route name an item links to; the record id goes in params.id." },
2041
+ "lateWhen": {
2042
+ "type": "object",
2043
+ "additionalProperties": false,
2044
+ "description": "Rule on the number of days until the date that marks an item late. Default: before today ({ op: 'lt', value: 0 }).",
2045
+ "properties": {
2046
+ "op": { "type": "string", "enum": ["eq", "neq", "gt", "gte", "lt", "lte"] },
2047
+ "value": { "type": "number" }
2048
+ }
2049
+ },
2050
+ "lateField": { "type": "string", "description": "A boolean field that marks a record late, whatever its date." },
2051
+ "items": {
2052
+ "type": "array",
2053
+ "description": "Static items, used when there is no source.",
2054
+ "items": {
2055
+ "type": "object",
2056
+ "required": ["title", "date"],
2057
+ "additionalProperties": false,
2058
+ "properties": {
2059
+ "title": { "type": "string" },
2060
+ "meta": { "type": "string" },
2061
+ "date": { "type": "string", "description": "ISO date or date-time." },
2062
+ "route": { "oneOf": [{ "type": "string" }, { "type": "object", "additionalProperties": true }] },
2063
+ "href": { "type": "string" },
2064
+ "late": { "type": "boolean" }
2065
+ }
2066
+ }
2067
+ },
2068
+ "emptyText": { "type": "string", "description": "Text of a day without items." },
2069
+ "weekOffset": { "type": "integer", "description": "Whole weeks from the current one (1 = next week)." }
2070
+ }
2071
+ }
2072
+ }
2073
+ }
2074
+ }
2075
+ }
2076
+ },
2077
+ {
2078
+ "description": "stacked-bar catalog widget (CnStackedBarWidget): one segmented bar with a legend that carries the label and count of every segment. Takes a `props.content` blob. Segments come from one grouped count request (`source.groupBy`) or from static `segments`.",
2079
+ "if": {
2080
+ "properties": { "widgetKey": { "const": "stacked-bar" } },
2081
+ "required": ["widgetKey"]
2082
+ },
2083
+ "then": {
2084
+ "properties": {
2085
+ "props": {
2086
+ "type": "object",
2087
+ "properties": {
2088
+ "content": {
2089
+ "type": "object",
2090
+ "additionalProperties": true,
2091
+ "properties": {
2092
+ "source": {
2093
+ "type": "object",
2094
+ "additionalProperties": false,
2095
+ "required": ["register", "schema", "groupBy"],
2096
+ "description": "OpenRegister grouped count; `filter` supports the shared @-token grammar.",
2097
+ "properties": {
2098
+ "register": { "type": "string" },
2099
+ "schema": { "type": "string" },
2100
+ "groupBy": { "type": "string", "description": "The field the records are counted per." },
2101
+ "filter": { "type": "object", "additionalProperties": true }
2102
+ }
2103
+ },
2104
+ "segments": {
2105
+ "type": "array",
2106
+ "description": "Static segments, used when there is no source.",
2107
+ "items": {
2108
+ "type": "object",
2109
+ "required": ["label", "value"],
2110
+ "additionalProperties": false,
2111
+ "properties": {
2112
+ "key": { "type": "string" },
2113
+ "label": { "type": "string" },
2114
+ "value": { "type": "number", "minimum": 0 }
2115
+ }
2116
+ }
2117
+ },
2118
+ "order": { "type": "array", "items": { "type": "string" }, "description": "Group keys in the order they render. Unlisted groups follow; a listed key without records shows 0." },
2119
+ "labels": { "type": "object", "additionalProperties": { "type": "string" }, "description": "The label of a group key." },
2120
+ "emptyText": { "type": "string", "description": "Shown when the total is 0." }
2121
+ }
2122
+ }
2123
+ }
2124
+ }
2125
+ }
2126
+ }
1878
2127
  }
1879
2128
  ]
1880
2129
  },
@@ -2264,6 +2513,16 @@
2264
2513
  "type": "string",
2265
2514
  "description": "i18n key naming the `children` dropdown. Falls back to the parent action's own label, so the chevron is never announced unlabelled."
2266
2515
  },
2516
+ "group": {
2517
+ "type": "string",
2518
+ "minLength": 1,
2519
+ "description": "Detail page header actions only. i18n key / text naming the group this action sits in inside the overflow menu. Grouped actions render under a caption with this name, in first appearance order, after the ungrouped ones. Ignored on other surfaces."
2520
+ },
2521
+ "adminOnly": {
2522
+ "type": "boolean",
2523
+ "default": false,
2524
+ "description": "Detail page header actions only. Shows the action to instance admins alone, in the last group of the overflow menu. Hides a control and nothing more: the endpoint behind the action still decides who may call it. Ignored on other surfaces."
2525
+ },
2267
2526
  "visibleWhen": {
2268
2527
  "$ref": "#/$defs/visibleWhen",
2269
2528
  "description": "#91 Wave 3: an optional predicate gating whether the action renders — the shared banner shape ({ endpoint | source | field, op, value }). The `field`-only LOCAL form evaluates against the page object context (a detail action shown only in the right lifecycle state); endpoint / source forms fetch. Fail-safe: a broken predicate hides the action."
@@ -2887,7 +3146,7 @@
2887
3146
  },
2888
3147
  "columns": {
2889
3148
  "type": "array",
2890
- "description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate? }). `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link).",
3149
+ "description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate? }). `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
2891
3150
  "items": {
2892
3151
  "oneOf": [
2893
3152
  { "type": "string" },
@@ -2936,6 +3195,127 @@
2936
3195
  "additionalProperties": true
2937
3196
  }
2938
3197
  },
3198
+ "stageField": {
3199
+ "type": "string",
3200
+ "minLength": 1,
3201
+ "default": "status",
3202
+ "description": "type='detail': dot-path to the field holding the record's stage. Read by `primaryActionByStage` and `nextStep`."
3203
+ },
3204
+ "primaryActionByStage": {
3205
+ "type": "object",
3206
+ "description": "type='detail': the page's primary button per stage. Keys are values of `stageField` (matched exactly first, then ignoring case). A value is an action in the header action shape, or the id of a `headerActions` entry, which then leaves the overflow menu. A stage without an entry falls back to the page's `primaryAction`.",
3207
+ "additionalProperties": {
3208
+ "oneOf": [
3209
+ { "type": "string", "minLength": 1 },
3210
+ { "$ref": "#/$defs/action" }
3211
+ ]
3212
+ }
3213
+ },
3214
+ "quickActions": {
3215
+ "type": "array",
3216
+ "maxItems": 3,
3217
+ "description": "type='detail': at most three always visible buttons in the header, for the actions a handler uses on every record. An item is an action in the header action shape, or the id of a `headerActions` entry, which then leaves the overflow menu.",
3218
+ "items": {
3219
+ "oneOf": [
3220
+ { "type": "string", "minLength": 1 },
3221
+ { "$ref": "#/$defs/action" }
3222
+ ]
3223
+ }
3224
+ },
3225
+ "actionsMenu": {
3226
+ "type": "object",
3227
+ "additionalProperties": false,
3228
+ "description": "type='detail': what the header's overflow menu itself shows. Omitted, the menu keeps Refresh and the three help links.",
3229
+ "properties": {
3230
+ "showRefresh": {
3231
+ "type": "boolean",
3232
+ "description": "false removes Refresh from the menu."
3233
+ },
3234
+ "showHelpLinks": {
3235
+ "type": "boolean",
3236
+ "description": "false removes Request a feature, Report a bug and Documentation, for an app that offers them in its own help menu."
3237
+ },
3238
+ "label": {
3239
+ "type": "string",
3240
+ "minLength": 1,
3241
+ "description": "i18n key / text naming the menu, for example 'More'. Defaults to 'Actions'."
3242
+ }
3243
+ }
3244
+ },
3245
+ "nextStep": {
3246
+ "type": "object",
3247
+ "additionalProperties": false,
3248
+ "required": ["stages"],
3249
+ "description": "type='detail': the 'what now' card above the body. For the record's stage it shows a short checklist next to the primary button. A stage without an entry shows no card. Rendered by CnNextStepCard.",
3250
+ "properties": {
3251
+ "field": {
3252
+ "type": "string",
3253
+ "minLength": 1,
3254
+ "description": "Dot-path to the stage, when it differs from `stageField`."
3255
+ },
3256
+ "title": {
3257
+ "type": "string",
3258
+ "description": "i18n key / heading used by a stage that names none."
3259
+ },
3260
+ "stages": {
3261
+ "type": "object",
3262
+ "description": "Keys are stage values.",
3263
+ "additionalProperties": {
3264
+ "type": "object",
3265
+ "additionalProperties": false,
3266
+ "required": ["checklist"],
3267
+ "properties": {
3268
+ "title": { "type": "string", "description": "i18n key / heading of the card for this stage." },
3269
+ "after": { "type": "string", "description": "i18n key / one line under the button saying what follows." },
3270
+ "checklist": {
3271
+ "type": "array",
3272
+ "description": "What this stage still needs. An empty list shows no card.",
3273
+ "items": {
3274
+ "type": "object",
3275
+ "additionalProperties": false,
3276
+ "required": ["label"],
3277
+ "properties": {
3278
+ "label": { "type": "string", "minLength": 1, "description": "i18n key / text of the item." },
3279
+ "hint": { "type": "string", "description": "i18n key / short note after the label, for example '2 of 5 done'." },
3280
+ "doneField": { "type": "string", "minLength": 1, "description": "Dot-path on the record. The item is done when it holds a value; a list counts when it is not empty." },
3281
+ "doneWhen": { "type": "object", "description": "A local visibleWhen condition ({ field, op, value } or { all | any }). The item is done when it is true. Wins over `doneField`." },
3282
+ "done": { "type": "boolean", "description": "A fixed state, for an item the record cannot answer." }
3283
+ }
3284
+ }
3285
+ }
3286
+ }
3287
+ }
3288
+ }
3289
+ }
3290
+ },
3291
+ "typePill": {
3292
+ "$ref": "#/$defs/headerPill",
3293
+ "description": "type='detail': a pill above the title saying what kind of record this is."
3294
+ },
3295
+ "statusPill": {
3296
+ "$ref": "#/$defs/headerPill",
3297
+ "description": "type='detail': a pill above the title saying where the record stands."
3298
+ },
3299
+ "sideColumn": {
3300
+ "type": "array",
3301
+ "description": "type='detail': a column of cards beside the body, for the record's facts. An item is a widget definition, or the id of a widget declared on the page. The column stacks under the body on a narrow page.",
3302
+ "items": {
3303
+ "oneOf": [
3304
+ { "type": "string", "minLength": 1 },
3305
+ {
3306
+ "type": "object",
3307
+ "additionalProperties": true,
3308
+ "required": ["type"],
3309
+ "properties": {
3310
+ "id": { "type": "string" },
3311
+ "type": { "type": "string", "minLength": 1 },
3312
+ "title": { "type": "string" },
3313
+ "content": { "type": "object", "additionalProperties": true }
3314
+ }
3315
+ }
3316
+ ]
3317
+ }
3318
+ },
2939
3319
  "headerActions": {
2940
3320
  "type": "array",
2941
3321
  "description": "Page-level header actions rendered inside CnActionsBar's overflow dropdown (between the built-in Refresh action and the `#action-items` slot). On a `type:'detail'` page the same entries render in the header's Actions menu, above the Request-a-feature / Report-a-bug / Documentation trio. Same shape as the row-level `actions[]` ($ref action), but handlers receive NO row context — they are page-level. Reserved ids (`refresh`, `import`, `export`, `copy`, `delete`) are dropped at render time to avoid shadowing built-ins. See manifest-icons-and-page-actions for the dispatch semantics.",
@@ -2992,6 +3372,14 @@
2992
3372
  },
2993
3373
  "description": "type='index': saved view ids this page renders as tabs instead of as entries in the views control. A view appears in one place or the other, never both. An id naming a view that no longer exists produces no tab, rather than a tab that opens nothing. Maps to CnIndexPage `viewTabs`."
2994
3374
  },
3375
+ "viewCounts": {
3376
+ "description": "type='index': which saved views show how many records they match. `true` counts every saved view (tabs and the entries in the views control), an array counts only the view ids it lists. Off by default, so no count request is made. Quick filters opt in per entry with `showCount: true`. Maps to CnIndexPage `viewCounts`.",
3377
+ "oneOf": [
3378
+ { "type": "boolean" },
3379
+ { "type": "array", "items": { "type": "string", "minLength": 1 } }
3380
+ ],
3381
+ "default": false
3382
+ },
2995
3383
  "priorityField": {
2996
3384
  "type": "string",
2997
3385
  "description": "type='index': the field carrying a record's DERIVED priority. The list reads the value and sorts on it; it never computes one. A record with no priority, or one outside `priorityLevels`, sorts after every ranked record in BOTH directions and stays in the list. Maps to CnIndexPage `priorityField`."
@@ -342,7 +342,8 @@
342
342
  "label": { "type": "string", "description": "i18n key or plain text rendered on the tab." },
343
343
  "filter": { "type": "object", "additionalProperties": true, "description": "Filter map merged into the fetch when this tab is active." },
344
344
  "default": { "type": "boolean", "default": false, "description": "Pre-selected on mount." },
345
- "icon": { "type": "string", "description": "Optional MDI icon name displayed alongside the label." }
345
+ "icon": { "type": "string", "description": "Optional MDI icon name displayed alongside the label." },
346
+ "showCount": { "type": "boolean", "default": false, "description": "Show how many records this tab matches, after its label. Off by default: no count request is made for a tab that does not ask. Tabs that filter the same single field share one grouped request." }
346
347
  }
347
348
  }
348
349
  },
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * dateVariant: turn "how far away is this date" into a colour variant.
6
+ *
7
+ * A deadline reads differently depending on how close it is. The rules are the
8
+ * `variantWhen` shape the stat tile already uses (`[{ op, value, variant }]`,
9
+ * first match wins), compared against the number of CALENDAR days from today
10
+ * until the date: `0` is today, a negative number is overdue.
11
+ *
12
+ * ```js
13
+ * resolveDateVariant('2026-10-01', [
14
+ * { op: 'lt', value: 0, variant: 'error' }, // overdue
15
+ * { op: 'lte', value: 5, variant: 'warning' }, // within five days
16
+ * ])
17
+ * ```
18
+ *
19
+ * Shared by the `date` cell widget (CnCellRenderer) and the week strip, and
20
+ * meant for board cards too, so a deadline is coloured by one rule everywhere.
21
+ *
22
+ * @module utils/dateVariant
23
+ */
24
+
25
+ import { compareVisibleWhen } from './visibleWhen.js'
26
+
27
+ /** Variants a date rule may name. `danger` is accepted as an alias of `error`. */
28
+ export const DATE_VARIANTS = Object.freeze(['default', 'success', 'warning', 'error'])
29
+
30
+ const MS_PER_DAY = 86400000
31
+
32
+ /**
33
+ * Parse a value into a Date. A date-only string (`2026-10-05`) is read as a
34
+ * LOCAL day, so it does not slide to the day before in a timezone west of UTC.
35
+ *
36
+ * @param {unknown} value A Date, a timestamp, or an ISO string.
37
+ * @return {Date|null} The date, or null when the value is not one.
38
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells
39
+ */
40
+ export function parseDateValue(value) {
41
+ if (value === null || value === undefined || value === '') {
42
+ return null
43
+ }
44
+ if (value instanceof Date) {
45
+ return Number.isNaN(value.getTime()) ? null : value
46
+ }
47
+ if (typeof value === 'string') {
48
+ const dateOnly = value.match(/^(\d{4})-(\d{2})-(\d{2})$/)
49
+ if (dateOnly) {
50
+ return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))
51
+ }
52
+ }
53
+ if (typeof value !== 'string' && typeof value !== 'number') {
54
+ return null
55
+ }
56
+ const parsed = new Date(value)
57
+ return Number.isNaN(parsed.getTime()) ? null : parsed
58
+ }
59
+
60
+ /**
61
+ * The local calendar day of a date as `YYYY-MM-DD`.
62
+ *
63
+ * @param {Date} date The date.
64
+ * @return {string} The day key.
65
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-week-strip-widget
66
+ */
67
+ export function dayKey(date) {
68
+ const month = String(date.getMonth() + 1).padStart(2, '0')
69
+ const day = String(date.getDate()).padStart(2, '0')
70
+ return `${date.getFullYear()}-${month}-${day}`
71
+ }
72
+
73
+ /**
74
+ * Whole calendar days from `now` until `value`: 0 today, negative when past.
75
+ *
76
+ * @param {unknown} value The date.
77
+ * @param {Date} [now] The reference moment (defaults to the current time).
78
+ * @return {number|null} The day count, or null when `value` is not a date.
79
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells
80
+ */
81
+ export function daysUntil(value, now = new Date()) {
82
+ const date = parseDateValue(value)
83
+ if (date === null) {
84
+ return null
85
+ }
86
+ // UTC midnights of the two LOCAL days, so a daylight-saving hour never
87
+ // turns one day into zero or two.
88
+ const target = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
89
+ const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())
90
+ return Math.round((target - today) / MS_PER_DAY)
91
+ }
92
+
93
+ /**
94
+ * The variant of the first rule the date matches, or '' when none does.
95
+ *
96
+ * @param {unknown} value The date.
97
+ * @param {Array<{op: string, value: number, variant: string}>} rules The `variantWhen` rules.
98
+ * @param {Date} [now] The reference moment (defaults to the current time).
99
+ * @return {string} `success | warning | error | default`, or '' for no match.
100
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells
101
+ */
102
+ export function resolveDateVariant(value, rules, now = new Date()) {
103
+ if (!Array.isArray(rules) || rules.length === 0) {
104
+ return ''
105
+ }
106
+ const days = daysUntil(value, now)
107
+ if (days === null) {
108
+ return ''
109
+ }
110
+ for (const rule of rules) {
111
+ if (!rule || typeof rule !== 'object') {
112
+ continue
113
+ }
114
+ if (compareVisibleWhen(days, rule.op || 'eq', rule.value) === true) {
115
+ const variant = rule.variant === 'danger' ? 'error' : rule.variant
116
+ return DATE_VARIANTS.includes(variant) ? variant : ''
117
+ }
118
+ }
119
+ return ''
120
+ }