@conduction/nextcloud-vue 2.56.0-beta.46 → 2.56.0-beta.48

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 (199) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  17. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  18. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
  21. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
  23. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +111 -42
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +117 -73
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -39
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
  32. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  41. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  43. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  45. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  46. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  50. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  51. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  52. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  53. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  54. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +21 -0
  57. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  60. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  61. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
  65. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  67. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  68. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  70. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  72. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  73. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  74. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  75. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  76. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
  77. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
  78. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
  79. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
  80. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
  81. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
  82. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
  83. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
  84. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
  85. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
  86. package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
  87. package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
  88. package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -3
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  94. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  95. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  96. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  98. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
  99. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  100. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
  101. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  102. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  103. package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
  104. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  105. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  106. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  107. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
  108. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
  110. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  112. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  113. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  114. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  115. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  116. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  117. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  118. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  119. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
  120. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  121. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  122. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +32 -0
  123. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  124. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
  125. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  126. package/dist/esm/composables/indexSources.js +9 -0
  127. package/dist/esm/composables/indexSources.js.map +1 -1
  128. package/dist/esm/l10n/en.json.js +24 -1
  129. package/dist/esm/l10n/en.json.js.map +1 -1
  130. package/dist/esm/l10n/nl.json.js +24 -1
  131. package/dist/esm/l10n/nl.json.js.map +1 -1
  132. package/dist/esm/library-style.js +1 -1
  133. package/dist/esm/nextcloud-vue.css +1190 -410
  134. package/dist/esm/utils/dueText.js +49 -8
  135. package/dist/esm/utils/dueText.js.map +1 -1
  136. package/dist/esm/utils/featureCatalog.js +135 -0
  137. package/dist/esm/utils/featureCatalog.js.map +1 -0
  138. package/dist/esm/utils/fillTemplate.js +74 -3
  139. package/dist/esm/utils/fillTemplate.js.map +1 -1
  140. package/dist/esm/utils/headerMeta.js +62 -12
  141. package/dist/esm/utils/headerMeta.js.map +1 -1
  142. package/dist/esm/utils/roadmapIssues.js +86 -0
  143. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  144. package/dist/esm/utils/userDisplayName.js +47 -0
  145. package/dist/esm/utils/userDisplayName.js.map +1 -0
  146. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  147. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  148. package/dist/nextcloud-vue.cjs.js +16229 -13423
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +1190 -410
  151. package/l10n/en.json +24 -1
  152. package/l10n/nl.json +24 -1
  153. package/package.json +1 -1
  154. package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
  155. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
  156. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  157. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  158. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  159. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  160. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
  161. package/src/components/CnDashboardPage/CnDashboardPage.vue +141 -8
  162. package/src/components/CnDataTable/CnDataTable.md +1 -0
  163. package/src/components/CnDataTable/CnDataTable.vue +41 -46
  164. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  165. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  166. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  167. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  168. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  169. package/src/components/CnFormDialog/CnFormDialog.vue +606 -562
  170. package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
  171. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  172. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  173. package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
  174. package/src/components/CnPageHeader/CnPageHeader.md +11 -0
  175. package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
  176. package/src/components/CnPagination/CnPagination.vue +10 -2
  177. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +3 -3
  178. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  179. package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
  180. package/src/components/CnTabs/CnTabs.vue +39 -2
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
  182. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  183. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  184. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
  185. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +43 -2
  186. package/src/composables/indexSources.js +9 -0
  187. package/src/css/dialog.css +61 -0
  188. package/src/css/look-board-detail.css +8 -0
  189. package/src/css/look-board-index.css +27 -1
  190. package/src/css/look-board.css +60 -0
  191. package/src/css/page-header.css +16 -0
  192. package/src/schemas/app-manifest-v2.schema.json +30 -17
  193. package/src/utils/dueText.js +49 -8
  194. package/src/utils/featureCatalog.js +132 -0
  195. package/src/utils/fillTemplate.js +73 -2
  196. package/src/utils/headerMeta.js +62 -12
  197. package/src/utils/roadmapIssues.js +82 -0
  198. package/src/utils/userDisplayName.js +54 -0
  199. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -2,7 +2,10 @@ import { translate } from '@nextcloud/l10n';
2
2
  import { inject } from 'vue';
3
3
  import __sfc_main from '../CnWidgetGrid/CnWidgetIcon.vue.js';
4
4
  import { getByPath } from '../../composables/useEndpointSource.js';
5
+ import { normalizeLook } from '../../composables/useLook.js';
6
+ import { dispatchAction } from '../../utils/actionsDispatcher.js';
5
7
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js';
8
+ import { resolveDeepTokens } from '../../utils/resolveFilterTokens.js';
6
9
  import { userLocale } from '../../utils/userLocale.js';
7
10
 
8
11
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
@@ -80,8 +83,15 @@ var script = {
80
83
  * - `showDate` — render the absolute date underneath (default true).
81
84
  * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
82
85
  * - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
86
+ * - `daysLabel` — `{ one, other }` with `{count}`, the headline while days remain ("27 dagen").
87
+ * - `caption` — the line under the headline instead of the date, with `{date}` (the long date) and `{count}` (days left).
88
+ * - `startField` — property holding the start of the term; a bar then shows how much of it has passed.
89
+ * - `action` — one manifest action under the card (`{ label, type, variant?, ... }`, the headerActions shape; `variant: "primary"` draws it in the primary tint).
90
+ * - `emptyLabel` — the headline while the date is unset (default a dash).
91
+ * - `emptyCaption` — the line under it while the date is unset, with `{date}` read from `emptyDateField`.
92
+ * - `facts` — `[{ label, field }]` rows under the caption.
83
93
  *
84
- * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
94
+ * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string, daysLabel?: {one?: string, other?: string}, caption?: string, startField?: string, action?: object, emptyLabel?: string, emptyCaption?: string, emptyDateField?: string, facts?: Array<{label: string, field: string}> }}
85
95
  */
86
96
  content: {
87
97
  type: Object,
@@ -105,6 +115,8 @@ var script = {
105
115
  cnObjectContextInjected: inject('cnObjectContext', null),
106
116
  cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
107
117
  cnTranslate: inject('cnTranslate', null),
118
+ cnLook: inject('cnLook', 'nextcloud'),
119
+ cnDispatchAction: inject('cnDispatchAction', null),
108
120
  }
109
121
  },
110
122
 
@@ -196,7 +208,7 @@ var script = {
196
208
  }
197
209
  const days = this.daysRemaining;
198
210
  if (days === null) {
199
- return '—'
211
+ return this.content?.emptyLabel ? this.translate(this.content.emptyLabel) : '—'
200
212
  }
201
213
  if (days < 0) {
202
214
  const overdue = Math.abs(days);
@@ -207,12 +219,89 @@ var script = {
207
219
  if (days === 0) {
208
220
  return translate('nextcloud-vue', 'Due today')
209
221
  }
222
+ const labels = this.content?.daysLabel;
223
+ if (labels && typeof labels === 'object' && (labels.one || labels.other)) {
224
+ const template = this.translate((days === 1 ? labels.one : labels.other) || labels.other || labels.one);
225
+ return template.replace(/\{count\}/g, String(days))
226
+ }
210
227
  if (days === 1) {
211
228
  return translate('nextcloud-vue', '1 day left')
212
229
  }
213
230
  return translate('nextcloud-vue', '{count} days left', { count: days })
214
231
  },
215
232
 
233
+ /**
234
+ * Whether the app takes the board look: no icon circle.
235
+ *
236
+ * @return {boolean} True in the board look.
237
+ * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
238
+ */
239
+ isBoardLook() {
240
+ return normalizeLook(this.cnLook) === 'board'
241
+ },
242
+
243
+ /**
244
+ * How much of the term has passed, from `startField` to the date, in
245
+ * whole days and as a percentage (0 to 100), or null without a usable
246
+ * start.
247
+ *
248
+ * @return {number|null} The share passed.
249
+ * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
250
+ */
251
+ progress() {
252
+ const field = this.content?.startField;
253
+ if (!field || !this.record || !this.targetDate || this.isSettled) {
254
+ return null
255
+ }
256
+ const raw = getByPath(this.record, field);
257
+ const start = raw === null || raw === undefined || raw === '' ? null : new Date(raw);
258
+ if (!start || Number.isNaN(start.getTime())) {
259
+ return null
260
+ }
261
+ const startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
262
+ const span = startOfDay(this.targetDate) - startOfDay(start);
263
+ if (span <= 0) {
264
+ return null
265
+ }
266
+ return Math.min(100, Math.max(0, ((startOfDay(new Date()) - startOfDay(start)) / span) * 100))
267
+ },
268
+
269
+ /**
270
+ * The `facts` rows under the caption (`[{ label, field }]`), with
271
+ * their values read off the record; a row without a value is left out.
272
+ *
273
+ * @return {Array<{label: string, value: string}>} The rows.
274
+ * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
275
+ */
276
+ facts() {
277
+ const rows = Array.isArray(this.content?.facts) ? this.content.facts : [];
278
+ return rows
279
+ .filter((row) => row && typeof row.label === 'string' && typeof row.field === 'string')
280
+ .map((row) => {
281
+ const value = this.record ? getByPath(this.record, row.field) : null;
282
+ return { label: this.translate(row.label), value: value === null || value === undefined ? '' : this.translate(String(value)) }
283
+ })
284
+ .filter((row) => row.value !== '')
285
+ },
286
+
287
+ /** @return {string} The accessible name of the progress bar. */
288
+ progressLabel() {
289
+ return translate('nextcloud-vue', 'Time passed')
290
+ },
291
+
292
+ /**
293
+ * The label of `content.action`, translated, or ''.
294
+ *
295
+ * @return {string} The label.
296
+ * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
297
+ */
298
+ actionLabel() {
299
+ const action = this.content?.action;
300
+ return action && typeof action === 'object' && typeof action.label === 'string' && action.label !== ''
301
+ ? this.translate(action.label)
302
+ : ''
303
+ },
304
+
216
305
  /**
217
306
  * The absolute date under the headline, so the tile answers "how long"
218
307
  * and "when" at once.
@@ -220,10 +309,21 @@ var script = {
220
309
  * @return {string} The formatted date, or ''.
221
310
  */
222
311
  subLabel() {
312
+ if (!this.targetDate && typeof this.content?.emptyCaption === 'string' && this.content.emptyCaption !== '') {
313
+ const raw = this.content.emptyDateField && this.record ? getByPath(this.record, this.content.emptyDateField) : null;
314
+ const when = raw ? new Date(raw) : null;
315
+ const date = when && !Number.isNaN(when.getTime()) ? when.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' }) : '';
316
+ return this.translate(this.content.emptyCaption).replace(/\{date\}/g, date)
317
+ }
223
318
  if (!this.targetDate || this.content?.showDate === false) {
224
319
  return ''
225
320
  }
226
- return this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' })
321
+ const date = this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' });
322
+ const caption = this.content?.caption;
323
+ if (typeof caption === 'string' && caption !== '') {
324
+ return this.translate(caption).replace(/\{date\}/g, date).replace(/\{count\}/g, String(this.daysRemaining ?? ''))
325
+ }
326
+ return date
227
327
  },
228
328
 
229
329
  /**
@@ -252,6 +352,9 @@ var script = {
252
352
 
253
353
  /** @return {string} The value colour for the current band. */
254
354
  valueColor() {
355
+ if (this.isBoardLook && this.variant === 'default') {
356
+ return 'var(--color-main-text)'
357
+ }
255
358
  return VARIANT_COLORS[this.variant] || VARIANT_COLORS.default
256
359
  },
257
360
 
@@ -270,6 +373,39 @@ var script = {
270
373
  return { color: this.valueColor }
271
374
  },
272
375
  },
376
+
377
+ methods: {
378
+ /**
379
+ * The host's translate, or the text unchanged.
380
+ *
381
+ * @param {string} text The text.
382
+ * @return {string} The translation.
383
+ */
384
+ translate(text) {
385
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
386
+ },
387
+
388
+ /**
389
+ * Dispatch `content.action` with the record's tokens resolved, through
390
+ * the page's dispatcher when there is one.
391
+ *
392
+ * @return {unknown}
393
+ * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card
394
+ */
395
+ runAction() {
396
+ const action = this.content?.action;
397
+ if (!action || typeof action !== 'object') {
398
+ return undefined
399
+ }
400
+ const ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected) || {};
401
+ const tokenCtx = { ...ctx, object: this.record || ctx.object || null, objectId: this.record?.id ?? ctx.objectId ?? null };
402
+ const resolved = resolveDeepTokens(action, tokenCtx);
403
+ if (typeof this.cnDispatchAction === 'function') {
404
+ return this.cnDispatchAction(resolved)
405
+ }
406
+ return dispatchAction(resolved, { router: this.$router || null, tokenCtx })
407
+ },
408
+ },
273
409
  };
274
410
 
275
411
  export { script as default };
@@ -1 +1 @@
1
- {"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-kpi-card cn-countdown-widget\"\n\t\t:class=\"[`cn-countdown-widget--${variant}`, { 'cn-kpi-card--board cn-countdown-widget--board': isBoardLook }]\">\n\t\t<div v-if=\"!isBoardLook\" class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\" data-testid=\"cn-countdown-sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t\t<dl v-if=\"facts.length\" class=\"cn-countdown-widget__facts\" data-testid=\"cn-countdown-facts\">\n\t\t\t\t<template v-for=\"fact in facts\" :key=\"fact.label\">\n\t\t\t\t\t<dt>{{ fact.label }}</dt>\n\t\t\t\t\t<dd>{{ fact.value }}</dd>\n\t\t\t\t</template>\n\t\t\t</dl>\n\t\t\t<span\n\t\t\t\tv-if=\"progress !== null\"\n\t\t\t\tclass=\"cn-countdown-widget__progress\"\n\t\t\t\trole=\"meter\"\n\t\t\t\t:aria-label=\"progressLabel\"\n\t\t\t\t:aria-valuenow=\"Math.round(progress)\"\n\t\t\t\taria-valuemin=\"0\"\n\t\t\t\taria-valuemax=\"100\"\n\t\t\t\tdata-testid=\"cn-countdown-progress\">\n\t\t\t\t<span class=\"cn-countdown-widget__progress-fill\" :style=\"{ width: `${progress}%` }\" />\n\t\t\t</span>\n\t\t\t<button\n\t\t\t\tv-if=\"actionLabel\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-countdown-widget__action\"\n\t\t\t\t:class=\"{ 'cn-countdown-widget__action--primary': content.action && content.action.variant === 'primary' }\"\n\t\t\t\tdata-testid=\"cn-countdown-action\"\n\t\t\t\t@click=\"runAction\">\n\t\t\t\t{{ actionLabel }}\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t * - `daysLabel` — `{ one, other }` with `{count}`, the headline while days remain (\"27 dagen\").\n\t\t * - `caption` — the line under the headline instead of the date, with `{date}` (the long date) and `{count}` (days left).\n\t\t * - `startField` — property holding the start of the term; a bar then shows how much of it has passed.\n\t\t * - `action` — one manifest action under the card (`{ label, type, variant?, ... }`, the headerActions shape; `variant: \"primary\"` draws it in the primary tint).\n\t\t * - `emptyLabel` — the headline while the date is unset (default a dash).\n\t\t * - `emptyCaption` — the line under it while the date is unset, with `{date}` read from `emptyDateField`.\n\t\t * - `facts` — `[{ label, field }]` rows under the caption.\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string, daysLabel?: {one?: string, other?: string}, caption?: string, startField?: string, action?: object, emptyLabel?: string, emptyCaption?: string, emptyDateField?: string, facts?: Array<{label: string, field: string}> }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t\tcnLook: inject('cnLook', 'nextcloud'),\n\t\t\tcnDispatchAction: inject('cnDispatchAction', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn this.content?.emptyLabel ? this.translate(this.content.emptyLabel) : '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tconst labels = this.content?.daysLabel\n\t\t\tif (labels && typeof labels === 'object' && (labels.one || labels.other)) {\n\t\t\t\tconst template = this.translate((days === 1 ? labels.one : labels.other) || labels.other || labels.one)\n\t\t\t\treturn template.replace(/\\{count\\}/g, String(days))\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * Whether the app takes the board look: no icon circle.\n\t\t *\n\t\t * @return {boolean} True in the board look.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * How much of the term has passed, from `startField` to the date, in\n\t\t * whole days and as a percentage (0 to 100), or null without a usable\n\t\t * start.\n\t\t *\n\t\t * @return {number|null} The share passed.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tprogress() {\n\t\t\tconst field = this.content?.startField\n\t\t\tif (!field || !this.record || !this.targetDate || this.isSettled) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tconst start = raw === null || raw === undefined || raw === '' ? null : new Date(raw)\n\t\t\tif (!start || Number.isNaN(start.getTime())) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\tconst span = startOfDay(this.targetDate) - startOfDay(start)\n\t\t\tif (span <= 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn Math.min(100, Math.max(0, ((startOfDay(new Date()) - startOfDay(start)) / span) * 100))\n\t\t},\n\n\t\t/**\n\t\t * The `facts` rows under the caption (`[{ label, field }]`), with\n\t\t * their values read off the record; a row without a value is left out.\n\t\t *\n\t\t * @return {Array<{label: string, value: string}>} The rows.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tfacts() {\n\t\t\tconst rows = Array.isArray(this.content?.facts) ? this.content.facts : []\n\t\t\treturn rows\n\t\t\t\t.filter((row) => row && typeof row.label === 'string' && typeof row.field === 'string')\n\t\t\t\t.map((row) => {\n\t\t\t\t\tconst value = this.record ? getByPath(this.record, row.field) : null\n\t\t\t\t\treturn { label: this.translate(row.label), value: value === null || value === undefined ? '' : this.translate(String(value)) }\n\t\t\t\t})\n\t\t\t\t.filter((row) => row.value !== '')\n\t\t},\n\n\t\t/** @return {string} The accessible name of the progress bar. */\n\t\tprogressLabel() {\n\t\t\treturn t('nextcloud-vue', 'Time passed')\n\t\t},\n\n\t\t/**\n\t\t * The label of `content.action`, translated, or ''.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tactionLabel() {\n\t\t\tconst action = this.content?.action\n\t\t\treturn action && typeof action === 'object' && typeof action.label === 'string' && action.label !== ''\n\t\t\t\t? this.translate(action.label)\n\t\t\t\t: ''\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate && typeof this.content?.emptyCaption === 'string' && this.content.emptyCaption !== '') {\n\t\t\t\tconst raw = this.content.emptyDateField && this.record ? getByPath(this.record, this.content.emptyDateField) : null\n\t\t\t\tconst when = raw ? new Date(raw) : null\n\t\t\t\tconst date = when && !Number.isNaN(when.getTime()) ? when.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' }) : ''\n\t\t\t\treturn this.translate(this.content.emptyCaption).replace(/\\{date\\}/g, date)\n\t\t\t}\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst date = this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t\tconst caption = this.content?.caption\n\t\t\tif (typeof caption === 'string' && caption !== '') {\n\t\t\t\treturn this.translate(caption).replace(/\\{date\\}/g, date).replace(/\\{count\\}/g, String(this.daysRemaining ?? ''))\n\t\t\t}\n\t\t\treturn date\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\tif (this.isBoardLook && this.variant === 'default') {\n\t\t\t\treturn 'var(--color-main-text)'\n\t\t\t}\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The host's translate, or the text unchanged.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Dispatch `content.action` with the record's tokens resolved, through\n\t\t * the page's dispatcher when there is one.\n\t\t *\n\t\t * @return {unknown}\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\trunAction() {\n\t\t\tconst action = this.content?.action\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected) || {}\n\t\t\tconst tokenCtx = { ...ctx, object: this.record || ctx.object || null, objectId: this.record?.id ?? ctx.objectId ?? null }\n\t\t\tconst resolved = resolveDeepTokens(action, tokenCtx)\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(resolved)\n\t\t\t}\n\t\t\treturn dispatchAction(resolved, { router: this.$router || null, tokenCtx })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__progress {\n\tbackground: var(--color-border);\n\tborder-radius: 3px;\n\tdisplay: block;\n\theight: 6px;\n\tmargin-block-start: 10px;\n\toverflow: hidden;\n}\n\n.cn-countdown-widget__progress-fill {\n\tbackground: var(--color-primary-element);\n\tdisplay: block;\n\theight: 100%;\n}\n\n.cn-countdown-widget__action {\n\talign-self: flex-start;\n\tbackground: var(--color-background-dark);\n\tborder: 0;\n\tborder-radius: var(--border-radius-pill, 17px);\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n\tmargin-block-start: 10px;\n\tmin-height: 34px;\n\tpadding: 0 12px;\n}\n\n.cn-countdown-widget__action--primary {\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n}\n\n.cn-countdown-widget__facts {\n\tdisplay: grid;\n\tgap: 8px 16px;\n\tgrid-template-columns: max-content 1fr;\n\tmargin: 10px 0 0;\n}\n\n.cn-countdown-widget__facts dt {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-countdown-widget__facts dd {\n\tmargin: 0;\n}\n\n.cn-countdown-widget__action:hover,\n.cn-countdown-widget__action:focus-visible {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, normalizeClass, createElementVNode, normalizeStyle, createVNode, toDisplayString, createCommentVNode } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, normalizeStyle, createVNode, createCommentVNode, createElementVNode, toDisplayString, Fragment, renderList } from 'vue';
2
2
 
3
3
  const _hoisted_1 = { class: "cn-kpi-card__body" };
4
4
  const _hoisted_2 = {
@@ -7,24 +7,34 @@ const _hoisted_2 = {
7
7
  };
8
8
  const _hoisted_3 = {
9
9
  key: 1,
10
- class: "cn-countdown-widget__sub"
10
+ class: "cn-countdown-widget__sub",
11
+ "data-testid": "cn-countdown-sub"
11
12
  };
13
+ const _hoisted_4 = {
14
+ key: 2,
15
+ class: "cn-countdown-widget__facts",
16
+ "data-testid": "cn-countdown-facts"
17
+ };
18
+ const _hoisted_5 = ["aria-label", "aria-valuenow"];
12
19
 
13
20
  function render(_ctx, _cache, $props, $setup, $data, $options) {
14
21
  const _component_CnWidgetIcon = resolveComponent("CnWidgetIcon");
15
22
 
16
23
  return (openBlock(), createElementBlock("div", {
17
- class: normalizeClass(["cn-kpi-card cn-countdown-widget", `cn-countdown-widget--${$options.variant}`])
24
+ class: normalizeClass(["cn-kpi-card cn-countdown-widget", [`cn-countdown-widget--${$options.variant}`, { 'cn-kpi-card--board cn-countdown-widget--board': $options.isBoardLook }]])
18
25
  }, [
19
- createElementVNode("div", {
20
- class: "cn-kpi-card__icon cn-countdown-widget__icon",
21
- style: normalizeStyle($options.iconCircleStyle)
22
- }, [
23
- createVNode(_component_CnWidgetIcon, {
24
- name: $options.resolvedIcon,
25
- size: 24
26
- }, null, 8 /* PROPS */, ["name"])
27
- ], 4 /* STYLE */),
26
+ (!$options.isBoardLook)
27
+ ? (openBlock(), createElementBlock("div", {
28
+ key: 0,
29
+ class: "cn-kpi-card__icon cn-countdown-widget__icon",
30
+ style: normalizeStyle($options.iconCircleStyle)
31
+ }, [
32
+ createVNode(_component_CnWidgetIcon, {
33
+ name: $options.resolvedIcon,
34
+ size: 24
35
+ }, null, 8 /* PROPS */, ["name"])
36
+ ], 4 /* STYLE */))
37
+ : createCommentVNode("v-if", true),
28
38
  createElementVNode("div", _hoisted_1, [
29
39
  ($options.resolvedLabel)
30
40
  ? (openBlock(), createElementBlock("div", _hoisted_2, toDisplayString($options.resolvedLabel), 1 /* TEXT */))
@@ -35,6 +45,44 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
35
45
  }, toDisplayString($options.headline), 5 /* TEXT, STYLE */),
36
46
  ($options.subLabel)
37
47
  ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString($options.subLabel), 1 /* TEXT */))
48
+ : createCommentVNode("v-if", true),
49
+ ($options.facts.length)
50
+ ? (openBlock(), createElementBlock("dl", _hoisted_4, [
51
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.facts, (fact) => {
52
+ return (openBlock(), createElementBlock(Fragment, {
53
+ key: fact.label
54
+ }, [
55
+ createElementVNode("dt", null, toDisplayString(fact.label), 1 /* TEXT */),
56
+ createElementVNode("dd", null, toDisplayString(fact.value), 1 /* TEXT */)
57
+ ], 64 /* STABLE_FRAGMENT */))
58
+ }), 128 /* KEYED_FRAGMENT */))
59
+ ]))
60
+ : createCommentVNode("v-if", true),
61
+ ($options.progress !== null)
62
+ ? (openBlock(), createElementBlock("span", {
63
+ key: 3,
64
+ class: "cn-countdown-widget__progress",
65
+ role: "meter",
66
+ "aria-label": $options.progressLabel,
67
+ "aria-valuenow": Math.round($options.progress),
68
+ "aria-valuemin": "0",
69
+ "aria-valuemax": "100",
70
+ "data-testid": "cn-countdown-progress"
71
+ }, [
72
+ createElementVNode("span", {
73
+ class: "cn-countdown-widget__progress-fill",
74
+ style: normalizeStyle({ width: `${$options.progress}%` })
75
+ }, null, 4 /* STYLE */)
76
+ ], 8 /* PROPS */, _hoisted_5))
77
+ : createCommentVNode("v-if", true),
78
+ ($options.actionLabel)
79
+ ? (openBlock(), createElementBlock("button", {
80
+ key: 4,
81
+ type: "button",
82
+ class: normalizeClass(["cn-countdown-widget__action", { 'cn-countdown-widget__action--primary': $props.content.action && $props.content.action.variant === 'primary' }]),
83
+ "data-testid": "cn-countdown-action",
84
+ onClick: _cache[0] || (_cache[0] = (...args) => ($options.runAction && $options.runAction(...args)))
85
+ }, toDisplayString($options.actionLabel), 3 /* TEXT, CLASS */))
38
86
  : createCommentVNode("v-if", true)
39
87
  ])
40
88
  ], 2 /* CLASS */))
@@ -1 +1 @@
1
- {"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=d1b330c0&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
1
+ {"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=22ccd275&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-kpi-card cn-countdown-widget\"\n\t\t:class=\"[`cn-countdown-widget--${variant}`, { 'cn-kpi-card--board cn-countdown-widget--board': isBoardLook }]\">\n\t\t<div v-if=\"!isBoardLook\" class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\" data-testid=\"cn-countdown-sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t\t<dl v-if=\"facts.length\" class=\"cn-countdown-widget__facts\" data-testid=\"cn-countdown-facts\">\n\t\t\t\t<template v-for=\"fact in facts\" :key=\"fact.label\">\n\t\t\t\t\t<dt>{{ fact.label }}</dt>\n\t\t\t\t\t<dd>{{ fact.value }}</dd>\n\t\t\t\t</template>\n\t\t\t</dl>\n\t\t\t<span\n\t\t\t\tv-if=\"progress !== null\"\n\t\t\t\tclass=\"cn-countdown-widget__progress\"\n\t\t\t\trole=\"meter\"\n\t\t\t\t:aria-label=\"progressLabel\"\n\t\t\t\t:aria-valuenow=\"Math.round(progress)\"\n\t\t\t\taria-valuemin=\"0\"\n\t\t\t\taria-valuemax=\"100\"\n\t\t\t\tdata-testid=\"cn-countdown-progress\">\n\t\t\t\t<span class=\"cn-countdown-widget__progress-fill\" :style=\"{ width: `${progress}%` }\" />\n\t\t\t</span>\n\t\t\t<button\n\t\t\t\tv-if=\"actionLabel\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-countdown-widget__action\"\n\t\t\t\t:class=\"{ 'cn-countdown-widget__action--primary': content.action && content.action.variant === 'primary' }\"\n\t\t\t\tdata-testid=\"cn-countdown-action\"\n\t\t\t\t@click=\"runAction\">\n\t\t\t\t{{ actionLabel }}\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t * - `daysLabel` — `{ one, other }` with `{count}`, the headline while days remain (\"27 dagen\").\n\t\t * - `caption` — the line under the headline instead of the date, with `{date}` (the long date) and `{count}` (days left).\n\t\t * - `startField` — property holding the start of the term; a bar then shows how much of it has passed.\n\t\t * - `action` — one manifest action under the card (`{ label, type, variant?, ... }`, the headerActions shape; `variant: \"primary\"` draws it in the primary tint).\n\t\t * - `emptyLabel` — the headline while the date is unset (default a dash).\n\t\t * - `emptyCaption` — the line under it while the date is unset, with `{date}` read from `emptyDateField`.\n\t\t * - `facts` — `[{ label, field }]` rows under the caption.\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string, daysLabel?: {one?: string, other?: string}, caption?: string, startField?: string, action?: object, emptyLabel?: string, emptyCaption?: string, emptyDateField?: string, facts?: Array<{label: string, field: string}> }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t\tcnLook: inject('cnLook', 'nextcloud'),\n\t\t\tcnDispatchAction: inject('cnDispatchAction', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn this.content?.emptyLabel ? this.translate(this.content.emptyLabel) : '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tconst labels = this.content?.daysLabel\n\t\t\tif (labels && typeof labels === 'object' && (labels.one || labels.other)) {\n\t\t\t\tconst template = this.translate((days === 1 ? labels.one : labels.other) || labels.other || labels.one)\n\t\t\t\treturn template.replace(/\\{count\\}/g, String(days))\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * Whether the app takes the board look: no icon circle.\n\t\t *\n\t\t * @return {boolean} True in the board look.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * How much of the term has passed, from `startField` to the date, in\n\t\t * whole days and as a percentage (0 to 100), or null without a usable\n\t\t * start.\n\t\t *\n\t\t * @return {number|null} The share passed.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tprogress() {\n\t\t\tconst field = this.content?.startField\n\t\t\tif (!field || !this.record || !this.targetDate || this.isSettled) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tconst start = raw === null || raw === undefined || raw === '' ? null : new Date(raw)\n\t\t\tif (!start || Number.isNaN(start.getTime())) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\tconst span = startOfDay(this.targetDate) - startOfDay(start)\n\t\t\tif (span <= 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn Math.min(100, Math.max(0, ((startOfDay(new Date()) - startOfDay(start)) / span) * 100))\n\t\t},\n\n\t\t/**\n\t\t * The `facts` rows under the caption (`[{ label, field }]`), with\n\t\t * their values read off the record; a row without a value is left out.\n\t\t *\n\t\t * @return {Array<{label: string, value: string}>} The rows.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tfacts() {\n\t\t\tconst rows = Array.isArray(this.content?.facts) ? this.content.facts : []\n\t\t\treturn rows\n\t\t\t\t.filter((row) => row && typeof row.label === 'string' && typeof row.field === 'string')\n\t\t\t\t.map((row) => {\n\t\t\t\t\tconst value = this.record ? getByPath(this.record, row.field) : null\n\t\t\t\t\treturn { label: this.translate(row.label), value: value === null || value === undefined ? '' : this.translate(String(value)) }\n\t\t\t\t})\n\t\t\t\t.filter((row) => row.value !== '')\n\t\t},\n\n\t\t/** @return {string} The accessible name of the progress bar. */\n\t\tprogressLabel() {\n\t\t\treturn t('nextcloud-vue', 'Time passed')\n\t\t},\n\n\t\t/**\n\t\t * The label of `content.action`, translated, or ''.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\tactionLabel() {\n\t\t\tconst action = this.content?.action\n\t\t\treturn action && typeof action === 'object' && typeof action.label === 'string' && action.label !== ''\n\t\t\t\t? this.translate(action.label)\n\t\t\t\t: ''\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate && typeof this.content?.emptyCaption === 'string' && this.content.emptyCaption !== '') {\n\t\t\t\tconst raw = this.content.emptyDateField && this.record ? getByPath(this.record, this.content.emptyDateField) : null\n\t\t\t\tconst when = raw ? new Date(raw) : null\n\t\t\t\tconst date = when && !Number.isNaN(when.getTime()) ? when.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' }) : ''\n\t\t\t\treturn this.translate(this.content.emptyCaption).replace(/\\{date\\}/g, date)\n\t\t\t}\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst date = this.targetDate.toLocaleDateString(userLocale(), { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t\tconst caption = this.content?.caption\n\t\t\tif (typeof caption === 'string' && caption !== '') {\n\t\t\t\treturn this.translate(caption).replace(/\\{date\\}/g, date).replace(/\\{count\\}/g, String(this.daysRemaining ?? ''))\n\t\t\t}\n\t\t\treturn date\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\tif (this.isBoardLook && this.variant === 'default') {\n\t\t\t\treturn 'var(--color-main-text)'\n\t\t\t}\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The host's translate, or the text unchanged.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Dispatch `content.action` with the record's tokens resolved, through\n\t\t * the page's dispatcher when there is one.\n\t\t *\n\t\t * @return {unknown}\n\t\t * @spec openspec/changes/countdown-board-look/specs/countdown-board-look/spec.md#requirement-the-countdown-draws-the-boards-term-card\n\t\t */\n\t\trunAction() {\n\t\t\tconst action = this.content?.action\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected) || {}\n\t\t\tconst tokenCtx = { ...ctx, object: this.record || ctx.object || null, objectId: this.record?.id ?? ctx.objectId ?? null }\n\t\t\tconst resolved = resolveDeepTokens(action, tokenCtx)\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(resolved)\n\t\t\t}\n\t\t\treturn dispatchAction(resolved, { router: this.$router || null, tokenCtx })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__progress {\n\tbackground: var(--color-border);\n\tborder-radius: 3px;\n\tdisplay: block;\n\theight: 6px;\n\tmargin-block-start: 10px;\n\toverflow: hidden;\n}\n\n.cn-countdown-widget__progress-fill {\n\tbackground: var(--color-primary-element);\n\tdisplay: block;\n\theight: 100%;\n}\n\n.cn-countdown-widget__action {\n\talign-self: flex-start;\n\tbackground: var(--color-background-dark);\n\tborder: 0;\n\tborder-radius: var(--border-radius-pill, 17px);\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n\tmargin-block-start: 10px;\n\tmin-height: 34px;\n\tpadding: 0 12px;\n}\n\n.cn-countdown-widget__action--primary {\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n}\n\n.cn-countdown-widget__facts {\n\tdisplay: grid;\n\tgap: 8px 16px;\n\tgrid-template-columns: max-content 1fr;\n\tmargin: 10px 0 0;\n}\n\n.cn-countdown-widget__facts dt {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-countdown-widget__facts dd {\n\tmargin: 0;\n}\n\n.cn-countdown-widget__action:hover,\n.cn-countdown-widget__action:focus-visible {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_createVNode","_createElementBlock","_toDisplayString","_createElementVNode","_openBlock","_Fragment","_renderList","_normalizeClass"],"mappings":";;AAkBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;AAMgB,EAAA,KAAK,EAAA,0BAAA;AAAA,EAAA,aAAA,EAAA,kBAAA;;;;AAGtB,EAAA,KAAA,EAAA,4BAAA;AAAA,EAAA,aAAA,EAAA,oBAAA;;;;;;;;8DAfyB,CAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,uDAAA,QAAA,CAAA,WAAA,EAAA,CAAA,CAAA,CAAA;;MAClB,QAAW,CAAA,WAAA;wCAEjB,CAAA,KAAA,EAAA;;AAFmB,UAAA,KAAK,EAAC,6CAAsB;AAAA,UAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;UACnDA,WAAkB,CAAA,uBAAA,EAAA;AAAJ,YAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,YAAA,IAAA,EAAA,EAAA;;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADH,QAAc,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEM,CAAA,KAAA,EAAA;AAFD,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;OAED,QAAS,CAAA,QAAA;0CAEd,CAAA,KAAA,YAAA,EAAAD,eAAA,CADF,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;AAER,MAAA,CAAA,QAAA,CAAA,KAAA,CAAA,MAAA;AAAJ,WAAAE,SAAA,EAAA,EAAAH,kBAAA,CAKK,MALL,UAKK,EAAA;8BAJJA,kBAGW,CAAAI,QAAA,EAAA,IAAA,EAAAC,UAAA,CAHR,QAAA,CAAA,KAAA,EAAA,CAAA,IAAA,KAAA;;AAAA,gBAAA,GAAA,EAAA,IAAA,CAAA,KAAA;;gBACFH,kBAAsB,CAAA,IAAA,EAAA,IAAA,EAAAD,eAAA,CAAf,IAAI,CAAC;gBACZC,kBAAyB,CAAA,IAAA,EAAA,IAAA,EAAAD,eAAA,CAAlB,IAAI,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;;;;OAIZ,QAAQ,CAAA,QAAA,KAAA,IAAA;wBADdD,kBAUM,CAAA,MAAA,EAAA;;AARN,YAAA,KAAK,EAAC,+BAAY;AAClB,YAAA,IAAI,EAAC,OAAO;AACX,YAAA,YAAU,EAAE,QAAa,CAAA,aAAA;AACzB,YAAA,eAAI,EAAA,IAAA,CAAA,KAAA,CAAA,QAAA,CAAA,QAAA,CAAA;AACL,YAAA,eAAK,EAAA,GAAA;AACL,YAAA,eAAa,EAAC,KAAG;AACjB,YAAA,aAAW,EAAC,uBAAA;;YACZE,kBAAkC,CAAA,MAAA,EAAA;AAA5B,cAAA,KAAK,EAAC,oCAAsB;AAAA,cAAA,KAAA,6BAAA,QAAA,CAAA,QAAA,CAAA,CAAA,CAAA,EAAA,CAAA;;;;OAG5B,QAAW,CAAA,WAAA;wBADjBF;;AAEA,YAAA,IAAI,EAAC,QAAQ;YACb,KAAK,EAAAM,cAAA,CAAA,CAAC,6BAAC,EAAA,EAAA,sCAAA,EACJ,MAAA,CAAA,OAAA,CAAA,MAAA,IAAA,MAAA,CAAA,OAAA,CAAA,MAAA,CAAA,OAAA,KAAA,SAAA,EAAA,CAAA,CAAA;AACH,YAAA,aAAE,EAAA,qBAAA;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDashboardPage.vue2.js';
2
2
  import { render } from './CnDashboardPage.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-42bc14a4";
5
+ script.__scopeId = "data-v-4f9a30ca";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };