@conduction/nextcloud-vue 2.82.0 → 2.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  13. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  14. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  17. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  18. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  21. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
  22. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +14 -0
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  30. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  31. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  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/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  42. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  43. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  50. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
  53. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
  55. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  57. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
  58. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
  61. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
  62. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
  64. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  70. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
  71. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  72. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
  73. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  74. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
  75. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
  76. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
  78. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  83. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  84. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +4 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  89. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
  90. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
  92. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
  94. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
  95. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
  97. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  99. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  106. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  107. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  108. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  109. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +16 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +41 -4
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +900 -537
  116. package/dist/esm/utils/buildManifest.js +37 -3
  117. package/dist/esm/utils/buildManifest.js.map +1 -1
  118. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  119. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  120. package/dist/esm/utils/fetchAggregate.js +25 -2
  121. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  122. package/dist/esm/utils/formatMetric.js +39 -3
  123. package/dist/esm/utils/formatMetric.js.map +1 -1
  124. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  125. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  126. package/dist/esm/utils/sentinelTokens.js +9 -0
  127. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  128. package/dist/esm/utils/switchValues.js +62 -0
  129. package/dist/esm/utils/switchValues.js.map +1 -0
  130. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  131. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  132. package/dist/nextcloud-vue.cjs.js +36711 -34909
  133. package/dist/nextcloud-vue.cjs.js.map +2 -2
  134. package/dist/nextcloud-vue.css +900 -537
  135. package/l10n/en.json +16 -1
  136. package/l10n/nl.json +41 -4
  137. package/package.json +1 -1
  138. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
  139. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  140. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  141. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  142. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
  143. package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
  144. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  145. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  146. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  147. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  148. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  149. package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
  150. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  151. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  152. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
  153. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
  154. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  155. package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
  156. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
  157. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  158. package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
  159. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
  160. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
  161. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  162. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  163. package/src/css/form-field.css +14 -0
  164. package/src/css/form-widget.css +11 -0
  165. package/src/css/index-page.css +11 -0
  166. package/src/css/index.css +1 -0
  167. package/src/css/look-board-reports.css +175 -0
  168. package/src/css/look-board.css +44 -1
  169. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  170. package/src/schemas/app-manifest-v2.schema.json +24 -3
  171. package/src/utils/buildManifest.js +36 -2
  172. package/src/utils/detailWidgetVisibility.js +145 -0
  173. package/src/utils/fetchAggregate.js +24 -1
  174. package/src/utils/formatMetric.js +38 -2
  175. package/src/utils/resolveManifestTokens.js +1 -0
  176. package/src/utils/sentinelTokens.js +9 -0
  177. package/src/utils/switchValues.js +59 -0
  178. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -43,6 +43,18 @@ const VARIANT_COLORS = Object.freeze({
43
43
  * negative number: "-3 days left" is a puzzle, and an overdue case is the one
44
44
  * state a handler must not have to decode.
45
45
  *
46
+ * ## A settled record
47
+ *
48
+ * `settledField` names a property on the record that is truthy once the work
49
+ * the date belongs to is done, such as a case in its final status. A settled
50
+ * record reads `settledLabel` (default "Closed") in no threshold colour, never
51
+ * "overdue": a closed case missed nothing that can still be acted on, and a red
52
+ * count of days on it sends a handler after a case that needs nobody.
53
+ *
54
+ * ```js
55
+ * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }
56
+ * ```
57
+ *
46
58
  * ## An unset date
47
59
  *
48
60
  * Renders a dash and no threshold colour. A case with no deadline is not urgent
@@ -65,8 +77,10 @@ var script = {
65
77
  * - `icon` — MDI icon name.
66
78
  * - `thresholds` — `{ warn, danger }` in days remaining.
67
79
  * - `showDate` — render the absolute date underneath (default true).
80
+ * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
81
+ * - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
68
82
  *
69
- * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}
83
+ * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
70
84
  */
71
85
  content: {
72
86
  type: Object,
@@ -89,6 +103,7 @@ var script = {
89
103
  return {
90
104
  cnObjectContextInjected: inject('cnObjectContext', null),
91
105
  cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
106
+ cnTranslate: inject('cnTranslate', null),
92
107
  }
93
108
  },
94
109
 
@@ -140,17 +155,44 @@ var script = {
140
155
  return Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)
141
156
  },
142
157
 
143
- /** @return {boolean} true once the date has passed. */
158
+ /**
159
+ * Whether the record says its work is done (`settledField` is truthy).
160
+ *
161
+ * @return {boolean} True for a settled record.
162
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
163
+ */
164
+ isSettled() {
165
+ const field = this.content?.settledField;
166
+ if (!this.record || !field) {
167
+ return false
168
+ }
169
+ return Boolean(getByPath(this.record, field))
170
+ },
171
+
172
+ /**
173
+ * Whether the date has passed on a record still open.
174
+ *
175
+ * @return {boolean} True when overdue.
176
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
177
+ */
144
178
  isOverdue() {
145
- return this.daysRemaining !== null && this.daysRemaining < 0
179
+ return !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0
146
180
  },
147
181
 
148
182
  /**
149
183
  * The headline: the remaining time in words.
150
184
  *
151
185
  * @return {string} The headline text.
186
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
152
187
  */
153
188
  headline() {
189
+ if (this.isSettled) {
190
+ const label = this.content?.settledLabel;
191
+ if (label) {
192
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label
193
+ }
194
+ return translate('nextcloud-vue', 'Closed')
195
+ }
154
196
  const days = this.daysRemaining;
155
197
  if (days === null) {
156
198
  return '—'
@@ -187,10 +229,11 @@ var script = {
187
229
  * The threshold band this date falls in.
188
230
  *
189
231
  * @return {string} One of default / success / warning / error.
232
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
190
233
  */
191
234
  variant() {
192
235
  const days = this.daysRemaining;
193
- if (days === null) {
236
+ if (days === null || this.isSettled) {
194
237
  return 'default'
195
238
  }
196
239
  if (days < 0) {
@@ -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'\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 * ## 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 *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\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}\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/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn 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 */\n\t\theadline() {\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(undefined, { 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 */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\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 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'\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(undefined, { 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 +1 @@
1
- {"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=17598258&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'\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 * ## 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 *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\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}\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/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn 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 */\n\t\theadline() {\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(undefined, { 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 */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\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=5546e71c&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'\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(undefined, { 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;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDashboardGrid.vue2.js';
2
2
  import { render } from './CnDashboardGrid.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-29653a76";
5
+ script.__scopeId = "data-v-ca2ae49c";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -29,6 +29,24 @@ let domIdCounter = 0;
29
29
  */
30
30
  const MENU_ANCHOR_OFFSET = 8;
31
31
 
32
+ /**
33
+ * How many GridStack rows a layout row is drawn as on a grid with
34
+ * size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.
35
+ */
36
+ const ROW_SPLIT = 8;
37
+
38
+ /**
39
+ * Whether a layout holds a size-to-content cell (`sizeToContent` true or a
40
+ * positive row maximum).
41
+ *
42
+ * @param {Array<object>} layout The layout items.
43
+ * @return {boolean} True when one does.
44
+ */
45
+ function hasFitCell(layout) {
46
+ return Array.isArray(layout) && layout.some((item) => item
47
+ && (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))
48
+ }
49
+
32
50
  /**
33
51
  * CnDashboardGrid — Low-level grid layout engine powered by GridStack.
34
52
  *
@@ -234,10 +252,38 @@ var script = {
234
252
  * on that.
235
253
  */
236
254
  reflowed: false,
255
+ /**
256
+ * How many GridStack rows one layout row is drawn as: 1, or
257
+ * ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,
258
+ * since GridStack reads the items' places from their attributes,
259
+ * and again when the grid is rebuilt.
260
+ */
261
+ rowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,
237
262
  }
238
263
  },
239
264
 
240
265
  computed: {
266
+ /**
267
+ * Whether the grid draws each layout row as ROW_SPLIT finer rows. A
268
+ * size-to-content cell can only be a whole number of rows tall, so on
269
+ * 80px rows it kept up to a row of empty space under its content and
270
+ * a packing grid (`float: false`) could not close it (pipelinq and
271
+ * dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the
272
+ * cell ends within 10px of its content and the widgets below move up.
273
+ *
274
+ * Only in view mode, only on a packing grid with a size-to-content
275
+ * cell: every other grid is built exactly as before. Item positions
276
+ * and sizes scale with the rows, so every widget keeps its pixel
277
+ * place and size until a cell shrinks; the layout itself, what edit
278
+ * mode shows and what `layout-change` reports, stays in layout rows.
279
+ *
280
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
281
+ * @return {boolean} True to split the rows.
282
+ */
283
+ wantsRowSplit() {
284
+ return !this.editable && !this.float && hasFitCell(this.layout)
285
+ },
286
+
241
287
  /**
242
288
  * @return {boolean} Whether the keyboard lane is live — items are
243
289
  * focusable and the arrow keys reposition. Requires the opt-in prop,
@@ -304,6 +350,12 @@ var script = {
304
350
  this.syncInteractivity();
305
351
  },
306
352
 
353
+ wantsRowSplit(wants) {
354
+ if (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {
355
+ this.rebuildGrid();
356
+ }
357
+ },
358
+
307
359
  layout: {
308
360
  deep: true,
309
361
  handler(newLayout) {
@@ -363,11 +415,75 @@ var script = {
363
415
  }
364
416
  // A number is GridStack's "size to content, but never taller than N rows" (a maximum).
365
417
  if (Number.isFinite(v) && v > 0) {
366
- return String(v)
418
+ return String(v * this.rowSplit)
367
419
  }
368
420
  return undefined
369
421
  },
370
422
 
423
+ /**
424
+ * A layout row coordinate or height in GridStack rows (`rowSplit`).
425
+ *
426
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
427
+ * @param {number|undefined} value The value in layout rows.
428
+ * @return {number|undefined} The value in grid rows, or undefined when none is set.
429
+ */
430
+ scaledRow(value) {
431
+ return Number.isFinite(value) ? value * this.rowSplit : undefined
432
+ },
433
+
434
+ /**
435
+ * The smallest height an item may take, in grid rows: `minHeight`
436
+ * layout rows, except for a size-to-content cell on split rows, which
437
+ * may shrink to its content down to one grid row. The minimum is for
438
+ * resizing by hand, and that only happens on whole rows in edit mode.
439
+ *
440
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
441
+ * @param {object} item The layout item.
442
+ * @return {number} The minimum height in grid rows.
443
+ */
444
+ minRows(item) {
445
+ if (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {
446
+ return 1
447
+ }
448
+ return this.minHeight * this.rowSplit
449
+ },
450
+
451
+ /**
452
+ * Build the grid again with the row split `wantsRowSplit` asks for, as
453
+ * edit mode turns on or off: the items go back to their layout places in
454
+ * the new rows, and the size-to-content cells are measured again.
455
+ *
456
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
457
+ * @return {void}
458
+ */
459
+ rebuildGrid() {
460
+ this.disconnectContentObserver();
461
+ this.grid.destroy(false);
462
+ this.grid = null;
463
+ this.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1;
464
+ this.$nextTick(() => {
465
+ if (!this.$refs.gridContainer) {
466
+ return
467
+ }
468
+ // GridStack wrote its own places onto the items; put the layout's back.
469
+ for (const item of this.layout) {
470
+ const el = this.$refs.gridContainer.querySelector(`[gs-id="${item.id}"]`);
471
+ if (!el) {
472
+ continue
473
+ }
474
+ for (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {
475
+ if (value === undefined) {
476
+ el.removeAttribute(name);
477
+ } else {
478
+ el.setAttribute(name, String(value));
479
+ }
480
+ }
481
+ }
482
+ this.initGrid();
483
+ this.observeContentSizes();
484
+ });
485
+ },
486
+
371
487
  /**
372
488
  * Build the accessible name for a grid item. A custom `itemLabel` wins
373
489
  * outright; otherwise the first human-readable field on the item is used
@@ -429,7 +545,7 @@ var script = {
429
545
  const el = this.$refs.gridContainer.querySelector('.grid-stack');
430
546
  this.grid = GridStack.init({
431
547
  column: this.columns,
432
- cellHeight: this.cellHeight,
548
+ cellHeight: this.cellHeight / this.rowSplit,
433
549
  margin: this.margin,
434
550
  float: this.float,
435
551
  animate: true,
@@ -516,6 +632,12 @@ var script = {
516
632
  if (this.isReflowed()) {
517
633
  return
518
634
  }
635
+ // A grid on split rows is a view-mode grid: what changes in it is
636
+ // packing and size-to-content measuring, never a user's
637
+ // arrangement, and it is not reported as one.
638
+ if (this.rowSplit !== 1) {
639
+ return
640
+ }
519
641
  if (!items || items.length === 0) {
520
642
  return
521
643
  }
@@ -916,7 +1038,7 @@ var script = {
916
1038
  // grid's own position back, which matches and does
917
1039
  // nothing here.
918
1040
  const x = item.gridX ?? node.x;
919
- const y = item.gridY ?? node.y;
1041
+ const y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y;
920
1042
  if (x !== node.x || y !== node.y) {
921
1043
  // The layout said where it goes; GridStack's `change`
922
1044
  // for it is not a user's arrangement and is not