@conduction/nextcloud-vue 2.83.0 → 2.85.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 (246) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  11. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  16. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  17. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBodySections/CnBodySections.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 +47 -4
  21. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
  25. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
  27. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  34. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  35. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  42. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  46. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  49. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  50. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  52. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  54. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  55. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  62. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  63. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  64. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  65. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  66. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  67. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
  68. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  70. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  76. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  77. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  78. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  79. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  80. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  81. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  82. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  84. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  85. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  86. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  87. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  88. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  89. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  90. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  91. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  93. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  94. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  95. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
  96. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  97. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  98. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  100. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  101. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  102. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  103. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  104. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  105. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
  106. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  109. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  110. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  111. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  112. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  113. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  114. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  115. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  116. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  117. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  118. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  119. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  120. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  121. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  122. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  123. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  124. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  125. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  126. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  127. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  128. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  129. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  130. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  131. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  132. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  133. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  134. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  135. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  136. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  137. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  138. package/dist/esm/composables/useListView.js +21 -0
  139. package/dist/esm/composables/useListView.js.map +1 -1
  140. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  141. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  142. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  143. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  144. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  145. package/dist/esm/index.js +1 -0
  146. package/dist/esm/index.js.map +1 -1
  147. package/dist/esm/l10n/en.json.js +38 -1
  148. package/dist/esm/l10n/en.json.js.map +1 -1
  149. package/dist/esm/l10n/nl.json.js +78 -1
  150. package/dist/esm/l10n/nl.json.js.map +1 -1
  151. package/dist/esm/library-style.js +1 -1
  152. package/dist/esm/nextcloud-vue.css +1030 -533
  153. package/dist/esm/store/useObjectStore.js +16 -2
  154. package/dist/esm/store/useObjectStore.js.map +1 -1
  155. package/dist/esm/utils/boardColumns.js +8 -1
  156. package/dist/esm/utils/boardColumns.js.map +1 -1
  157. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  158. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  159. package/dist/esm/utils/buildManifest.js +41 -4
  160. package/dist/esm/utils/buildManifest.js.map +1 -1
  161. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  162. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  163. package/dist/esm/utils/formatMetric.js +39 -3
  164. package/dist/esm/utils/formatMetric.js.map +1 -1
  165. package/dist/esm/utils/gridStack.js +40 -1
  166. package/dist/esm/utils/gridStack.js.map +1 -1
  167. package/dist/esm/utils/humaniseKey.js +41 -0
  168. package/dist/esm/utils/humaniseKey.js.map +1 -0
  169. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  170. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  171. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  172. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  173. package/dist/esm/utils/sentinelTokens.js +9 -0
  174. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  175. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  176. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  177. package/dist/esm/utils/widgetDispatch.js +8 -2
  178. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  179. package/dist/nextcloud-vue.cjs.js +17159 -13422
  180. package/dist/nextcloud-vue.cjs.js.map +2 -2
  181. package/dist/nextcloud-vue.css +1030 -533
  182. package/l10n/en.json +38 -1
  183. package/l10n/nl.json +78 -1
  184. package/package.json +1 -1
  185. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  186. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  187. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  188. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  189. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  190. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
  191. package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
  192. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  193. package/src/components/CnDetailPage/CnDetailPage.md +3 -1
  194. package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
  195. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  196. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  197. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  198. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  199. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  200. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  201. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  202. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  203. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
  204. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
  205. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  206. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  207. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  208. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  209. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  210. package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
  211. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  212. package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
  213. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  214. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  215. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  216. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  217. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  218. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  219. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  220. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  221. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  222. package/src/composables/useListView.js +21 -0
  223. package/src/css/board.css +65 -4
  224. package/src/css/dialog.css +4 -2
  225. package/src/css/form-field.css +14 -0
  226. package/src/css/form-widget.css +11 -0
  227. package/src/css/index-page.css +49 -0
  228. package/src/css/kpi-card.css +13 -0
  229. package/src/css/look-board-reports.css +58 -0
  230. package/src/css/look-board.css +108 -7
  231. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  232. package/src/index.js +2 -0
  233. package/src/schemas/app-manifest-v2.schema.json +137 -6
  234. package/src/store/useObjectStore.js +16 -2
  235. package/src/utils/boardColumns.js +8 -1
  236. package/src/utils/boardReferenceColumns.js +274 -0
  237. package/src/utils/buildManifest.js +40 -3
  238. package/src/utils/detailWidgetVisibility.js +145 -0
  239. package/src/utils/formatMetric.js +38 -2
  240. package/src/utils/gridStack.js +39 -0
  241. package/src/utils/humaniseKey.js +38 -0
  242. package/src/utils/notificationRuleLabel.js +5 -23
  243. package/src/utils/resolveManifestTokens.js +1 -0
  244. package/src/utils/sentinelTokens.js +9 -0
  245. package/src/utils/validateManifestV2.compiled.js +3 -3
  246. package/src/utils/widgetDispatch.js +8 -2
@@ -2,7 +2,7 @@ import script from './CnDeltaWidget.vue2.js';
2
2
  import { render } from './CnDeltaWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-66680dc6";
5
+ script.__scopeId = "data-v-ee481872";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -457,7 +457,7 @@ var script = {
457
457
  * @return {string} The formatted string.
458
458
  */
459
459
  formatNumber(value) {
460
- return formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))
460
+ return formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)
461
461
  },
462
462
 
463
463
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"CnDeltaWidget.vue2.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\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 * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["CnWidgetIcon","TrendingUp"],"mappings":"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"}
1
+ {"version":3,"file":"CnDeltaWidget.vue2.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\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 * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["CnWidgetIcon","TrendingUp"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnDeltaWidget.vue3.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue?vue&type=template&id=66680dc6&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\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 * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["_createBlock","_mergeProps","_createElementBlock","_createVNode","_Fragment","_openBlock","_resolveDynamicComponent","_createTextVNode"],"mappings":";;;;AA8BQ,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mDAAmD,GAAA;;;;AAerD,EAAA,KAAA,EAAA,6CAAA;;;;;;;uBApCTA,oCACI,IAAO,CAAA,OAAA,CAAA,EADXC;SAEK,GAAA,6BAA6B,EAAA;AAClC,GAAA,EAAA,wBAAA,EAAA,IAAA,CAAA,QAAA,6BAAA,IAAA,CAAA,QAAA,EAAA;qBAAA,QAAA,CAAA,UAAA;GAAA,QAAA,CAAA,IAAA,GAAA,mBAAA,GAAA,qBAAA;;KAKO,IAAS,CAAA,SAAA,CAAA,EAAA;qBAChB,MAKK;AAJD,MAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA;wBADJC,kBAKK,CAAA,KAAA,EAAA;;;AAFJ,YAAA,KAAK,iBAAE,QAAe,CAAA,eAAA,CAAA;;YACvBC,WAAgD,CAAA,uBAAA,EAAA;cAAjC,IAAI,EAAE,MAAO,CAAA,OAAA,CAAC,IAAI;AAAG,cAAA,IAAI,EAAE,EAAE;;;;;AAIjC,QAAA,CAAA,MAAA,CAAA,OAAO,CAAC,KAAK;0BAAxBD;;AAA0B,cAAA,KAAK,EAAC,2CAA8B;AAAA,cAAA,KAAA,EAAA,QAAA,CAAA,aAAA;+BAC1D,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;2BAgBZ,iBAAA,EAAA;WAZC,QAAA,CAAA,cAAA;uCAAA,CAAA,wBAAA,EAAA;;AAAA,gBAAA,IAAA,EAAA,EAAA;;eACY,QAAQ,CAAA,YAAA;8BAAvBA,kBAAuB,CAAA,MAAA,EAAA;;AAAA,kBAAA,KAAA,EAAA,wBAAA;AAAA,kBAAA,KAAA,EAAA,QAAA,CAAA,YAAA;mBAAA,GAAA,EAAA,CAAA,cAAA,UAAA,CAAA;gDAUd,CAAAE,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;oCARqB,CAAA,MAAA,EAAA;AAAzB,oBAAA,KAAK,EAAC,2CAAmB;AAAA,oBAAA,KAAA,iBAAA,QAAA,CAAA,UAAA,CAAA;qCAAA,QAAA,CAAA,gBAAA,CAAA,EAAA,CAAA,mBAAA;mBAExB,QAAQ,CAAA,QAAA,KAAA,IAAA;oCADdF,kBAMM,CAAA,MAAA,EAAA;;AAJN,wBAAA,KAAK,EAAA,wBAAA;AACJ,wBAAA,KAAK,0BAAE,QAAA,CAAA,UAAA,EAAA,CAAA;;AACR,yBAAAG,SAAA,EAAA,EAAAL,WAAA,CAAEM,uBAAA,CAAA,QAAA,CAAA,SAAA,CAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;AAAA,wBAAAC,eAAA,CAAA;;;;;AAMI,QAAA,CAAA,CAAA,QAAA,CAAA,cAAA,IAAA,CAAA,QAAA,CAAA,YAAA,IAAA,MAAA,CAAA,OAAA,CAAA,OAAA;0BAATL,0BAAA;;;;;;;;;;"}
1
+ {"version":3,"file":"CnDeltaWidget.vue3.js","sources":["../../../../src/components/CnDeltaWidget/CnDeltaWidget.vue?vue&type=template&id=ee481872&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-kpi-card cn-delta-widget\"\n\t\t:class=\"[\n\t\t\t{ 'cn-kpi-card--clickable': isLinked, 'cn-delta-widget--linked': isLinked },\n\t\t\t'cn-kpi-card--' + cardLayout,\n\t\t\tflat ? 'cn-kpi-card--flat' : 'cn-kpi-card--filled',\n\t\t]\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div\n\t\t\tv-if=\"content.icon\"\n\t\t\tclass=\"cn-kpi-card__icon cn-delta-widget__icon\"\n\t\t\t:style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"content.icon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body cn-delta-widget__body\">\n\t\t\t<div v-if=\"content.label\" class=\"cn-kpi-card__title cn-delta-widget__label\" :title=\"resolvedLabel\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-kpi-card__value-row cn-delta-widget__value-row\">\n\t\t\t\t<NcLoadingIcon v-if=\"displayLoading\" :size=\"22\" />\n\t\t\t\t<span v-else-if=\"displayError\" class=\"cn-delta-widget__error\" :title=\"displayError\">—</span>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<span class=\"cn-kpi-card__value cn-delta-widget__value\" :style=\"valueStyle\">{{ formattedCurrent }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"deltaPct !== null\"\n\t\t\t\t\t\tclass=\"cn-delta-widget__delta\"\n\t\t\t\t\t\t:style=\"{ color: deltaColor }\">\n\t\t\t\t\t\t<component :is=\"deltaIcon\" :size=\"16\" />\n\t\t\t\t\t\t{{ formattedDelta }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!displayLoading && !displayError && content.caption\" class=\"cn-kpi-card__label cn-delta-widget__caption\">\n\t\t\t\t{{ resolvedCaption }}\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport TrendingDown from 'vue-material-design-icons/TrendingDown.vue'\nimport TrendingNeutral from 'vue-material-design-icons/TrendingNeutral.vue'\nimport TrendingUp from 'vue-material-design-icons/TrendingUp.vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { dropOptionalUnresolved, resolveFilterTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n// The canonical KPI card, shared with CnStatWidget and CnStatsBlock.\n// Imported here so the tile is styled even when the consuming app pulls in\n// components individually rather than through css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnDeltaWidget — an abstract comparison / delta KPI tile.\n *\n * Reads TWO scalars from OpenRegister's aggregation `/value` endpoint — a\n * `current` value and a `previous` value — and renders the current value\n * alongside the percentage change between them, with a directional arrow tinted\n * good/bad according to `goodDirection`. Use it for period-over-period KPIs\n * (e.g. \"this month's revenue vs last month\") by combining relative filter\n * tokens (`@monthStart`, `@quarterStart`, …) in each leg's filter. Nothing is\n * hard-coded — every fleet app gets the same editable delta tile (ADR-041).\n *\n * Resolved by its registry type key `delta`; reference it from a manifest\n * placement `{ id, type: 'delta', content: { label, source, format, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Revenue (MTD)',\n * icon: 'Cash',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: {\n * register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value',\n * goodDirection: 'up',\n * current: { filter: { status: 'won', closedAt: { gte: '@monthStart' } } },\n * previous: { filter: { status: 'won', closedAt: { gte: '@monthStart-1mo', lt: '@monthStart' } } },\n * },\n * }\n * ```\n *\n * ENDPOINT BINDING (Wave 2, #91) — instead of the two OpenRegister legs, the\n * tile can read BOTH values from one app REST payload through the shared\n * `useEndpointSource` engine (token-resolved params, request dedup + TTL\n * cache, `cn:page:refresh` / `cn:widget:refresh` subscription). Exactly ONE\n * of `source` | `endpointSource` may be configured (validator-enforced).\n * The pipelinq `previousPeriod` overview contract:\n * ```js\n * content: {\n * label: 'Revenue',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * endpointSource: {\n * url: '/apps/pipelinq/api/analytics/commercial',\n * params: { period: '@workspace.datePreset?' },\n * },\n * valueField: 'revenue', // current value (dot-path)\n * previousField: 'previousPeriod.revenue', // previous value (dot-path)\n * // deltaField: 'revenueDeltaPct', // OR a server-computed percent\n * goodDirection: 'up',\n * }\n * ```\n */\nexport default {\n\tname: 'CnDeltaWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnWidgetIcon,\n\t\tTrendingUp,\n\t\tTrendingDown,\n\t\tTrendingNeutral,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object }`) for `@objectId` /\n\t\t * `@object.<field>` filter tokens. Null off a detail page.\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/**\n\t\t * v2 slot-grid detail context holder provided by CnPageRenderer —\n\t\t * backfills the object token context where CnDetailPage is not an\n\t\t * ancestor (#91 Wave 3).\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/**\n\t\t * Page-level workspace context (a reactive `{ <key>: value }` map)\n\t\t * provided by CnDashboardPage. Drives `@workspace.<param>` token\n\t\t * resolution — e.g. the date-range pills publish `dateFrom` / `dateTo`,\n\t\t * so a leg filter can scope to the picked window.\n\t\t */\n\t\tcnWorkspaceContext: { default: () => ({}) },\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` / `content.caption` are run\n\t\t * through it, exactly as CnStatWidget does. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location), `clickRoute` (Wave-2 alias), or `link`\n\t\t * (external href) turns the whole tile into a click-through target\n\t\t * (see the widgetLink mixin).\n\t\t *\n\t\t * Wave 2 (#91): `endpointSource` (`{ url, method?, params?,\n\t\t * responsePath? }`) reads BOTH legs from one app REST payload —\n\t\t * `valueField` plucks the current value, `previousField` the previous\n\t\t * value (the delta percent is computed client-side), or `deltaField`\n\t\t * supplies a server-computed percent directly. Exactly one of\n\t\t * `source` | `endpointSource`. `goodDirection` moves to the content\n\t\t * top level in endpoint mode (`source.goodDirection` still wins for\n\t\t * the OpenRegister form).\n\t\t *\n\t\t * @type {{label?: string, icon?: string, iconColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, goodDirection?: ('up'|'down'), current?: {filter?: object}, previous?: {filter?: object}}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down')}}\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 * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnDeltaWidget outside a CnAppRoot ancestor.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Endpoint binding (Wave 2): the shared useEndpointSource engine owns\n\t\t// token resolution, request dedup + TTL caching, and the\n\t\t// cn:page:refresh / cn:widget:refresh subscriptions. No-op while\n\t\t// `content.endpointSource` is absent, so the two-leg OpenRegister\n\t\t// path below is untouched. Injects re-read in setup (same resolution\n\t\t// as the Options `inject` block — the CnChartWidget precedent).\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst detailCtxRaw = inject('cnDetailObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', ref({}))\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst unwrap = (v) => ((v && typeof v === 'object' && 'value' in v) ? v.value : v)\n\t\tconst { data, loading, error, refetch } = useEndpointSource(\n\t\t\t() => (props.content && props.content.endpointSource) || null,\n\t\t\t{\n\t\t\t\tctx: () => ({\n\t\t\t\t\t...(resolveObjectTokenContext(objectCtxRaw, detailCtxRaw) || {}),\n\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\tconfig: unwrap(appConfigRaw) || {},\n\t\t\t\t}),\n\t\t\t},\n\t\t)\n\t\treturn { epData: data, epLoading: loading, epError: error, epRefetch: refetch }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcurrent: null,\n\t\t\tprevious: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The tile caption, run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedCaption() {\n\t\t\tconst caption = this.content.caption\n\t\t\treturn caption ? this.effectiveTranslate(caption) : ''\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped page-level app config map for `@config.*` token\n\t\t * resolution. Always an object (defaults to `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tconfigCtx() {\n\t\t\tconst c = this.cnAppConfig\n\t\t\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Inline style for the icon circle (tinted with iconColor). */\n\t\ticonCircleStyle() {\n\t\t\tconst color = this.content.iconColor || this.content.valueColor || 'var(--color-primary-element)'\n\t\t\treturn { color, backgroundColor: this.tint(color) }\n\t\t},\n\n\t\t/**\n\t\t * Card orientation. Horizontal (icon beside the number) is the\n\t\t * canonical KPI card; `content.layout: 'vertical'` stacks the icon\n\t\t * above a centred number.\n\t\t *\n\t\t * @return {'horizontal'|'vertical'}\n\t\t */\n\t\tcardLayout() {\n\t\t\treturn (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'\n\t\t},\n\n\t\t/**\n\t\t * Whether the card draws no box of its own. On by default — the tile\n\t\t * is rendered inside a CnWidgetWrapper that already draws a card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tflat() {\n\t\t\treturn (this.content || {}).flat !== false\n\t\t},\n\n\t\t/** Inline colour for the value, when the tile names one. */\n\t\tvalueStyle() {\n\t\t\treturn this.content.valueColor ? { color: this.content.valueColor } : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is endpoint-bound (Wave 2): a `content.endpointSource`\n\t\t * with a `url` reads both legs from one shared payload instead of the\n\t\t * two OpenRegister aggregation legs. Exactly one of `source` |\n\t\t * `endpointSource` is allowed (validator-enforced); endpointSource\n\t\t * wins when both slip through.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tendpointMode() {\n\t\t\tconst es = this.content.endpointSource\n\t\t\treturn !!(es && es.url)\n\t\t},\n\n\t\t/**\n\t\t * The current value for the active source: the payload value at\n\t\t * `content.valueField` in endpoint mode, else the OpenRegister\n\t\t * `current` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectiveCurrent() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.current\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.valueField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * The previous value for the active source: the payload value at\n\t\t * `content.previousField` in endpoint mode, else the OpenRegister\n\t\t * `previous` leg.\n\t\t *\n\t\t * @return {number|string|null}\n\t\t */\n\t\teffectivePrevious() {\n\t\t\tif (!this.endpointMode) {\n\t\t\t\treturn this.previous\n\t\t\t}\n\t\t\tconst v = getByPath(this.epData, this.content.previousField)\n\t\t\treturn v === undefined ? null : v\n\t\t},\n\n\t\t/**\n\t\t * Loading state for the active source (endpoint or OpenRegister).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tdisplayLoading() {\n\t\t\treturn this.endpointMode ? this.epLoading : this.loading\n\t\t},\n\n\t\t/**\n\t\t * Error message for the active source ('' = none).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayError() {\n\t\t\treturn this.endpointMode ? this.epError : this.error\n\t\t},\n\n\t\t/** The current value, number-formatted per content.format. */\n\t\tformattedCurrent() {\n\t\t\treturn this.formatNumber(this.effectiveCurrent)\n\t\t},\n\n\t\t/**\n\t\t * Percentage change current vs previous, or null when not computable.\n\t\t * In endpoint mode a `content.deltaField` (a server-computed percent\n\t\t * plucked from the payload) wins over the client-side computation.\n\t\t */\n\t\tdeltaPct() {\n\t\t\tif (this.endpointMode && this.content.deltaField) {\n\t\t\t\tconst v = Number(getByPath(this.epData, this.content.deltaField))\n\t\t\t\treturn Number.isFinite(v) ? v : null\n\t\t\t}\n\t\t\tconst prev = Number(this.effectivePrevious)\n\t\t\tconst cur = Number(this.effectiveCurrent)\n\t\t\tif (!Number.isFinite(prev) || prev === 0 || !Number.isFinite(cur)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn ((cur - prev) / Math.abs(prev)) * 100\n\t\t},\n\n\t\t/** The signed percentage, e.g. \"+12.3%\". */\n\t\tformattedDelta() {\n\t\t\tif (this.deltaPct === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst sign = this.deltaPct > 0 ? '+' : ''\n\t\t\treturn `${sign}${this.deltaPct.toFixed(1)}%`\n\t\t},\n\n\t\t/** The arrow component for the delta direction. */\n\t\tdeltaIcon() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'TrendingNeutral'\n\t\t\t}\n\t\t\treturn this.deltaPct > 0 ? 'TrendingUp' : 'TrendingDown'\n\t\t},\n\n\t\t/**\n\t\t * Green when the change is in the configured good direction, else red.\n\t\t * `source.goodDirection` (the OpenRegister form) wins; endpoint-bound\n\t\t * tiles declare `content.goodDirection` at the top level.\n\t\t */\n\t\tdeltaColor() {\n\t\t\tif (this.deltaPct === null || Math.abs(this.deltaPct) < 0.05) {\n\t\t\t\treturn 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\tconst good = (this.content.source && this.content.source.goodDirection)\n\t\t\t\t|| this.content.goodDirection || 'up'\n\t\t\tconst rising = this.deltaPct > 0\n\t\t\tconst isGood = good === 'up' ? rising : !rising\n\t\t\treturn isGood ? STATUS_TEXT_COLORS.success : STATUS_TEXT_COLORS.error\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\n\t\t/** Merged detail-page object context (`{ objectId, object, register, schema }`) or null — both detail-surface injects (#91 Wave 3). */\n\t\tobjectCtx() {\n\t\t\treturn resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext)\n\t\t},\n\n\t\t/** Unwrapped page-level workspace context map (always an object). */\n\t\tpageCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tconst v = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t\treturn v || {}\n\t\t},\n\n\t\t/** Workspace signature so the widget refetches when the date window changes. */\n\t\tworkspaceKey() {\n\t\t\treturn JSON.stringify(this.pageCtx || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\n\t\tworkspaceKey() {\n\t\t\tthis.fetchValues()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchValues()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive a faint background tint for the icon circle from a colour.\n\t\t *\n\t\t * @param {string} color The base colour (hex or CSS var).\n\t\t * @return {string} A translucent or token background.\n\t\t */\n\t\ttint(color) {\n\t\t\tif (typeof color === 'string' && /^#([0-9a-f]{6})$/i.test(color)) {\n\t\t\t\treturn color + '1f'\n\t\t\t}\n\t\t\treturn 'var(--color-primary-element-light, rgba(0,130,201,0.1))'\n\t\t},\n\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {number|string|null} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current and previous aggregates from OpenRegister.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchValues() {\n\t\t\t// Endpoint-bound tiles are fetched by the shared useEndpointSource\n\t\t\t// engine (see setup) — the two-leg OpenRegister path must not fire.\n\t\t\tif (this.endpointMode) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst base = { register: s.register, schema: s.schema, metric: s.metric, field: s.field }\n\t\t\t\t// Resolve `@workspace.*` (date-range pills) / `@objectId` / `@object.*`\n\t\t\t\t// per leg, then drop any optional `@workspace.<key>?` that stayed\n\t\t\t\t// unresolved (an unset window) so the leg falls back to its base\n\t\t\t\t// scope instead of sending a literal token. fetchAggregateValue does\n\t\t\t\t// NOT drop optional tokens itself, so we pre-resolve here.\n\t\t\t\tconst ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx }\n\t\t\t\tconst curFilter = dropOptionalUnresolved(resolveFilterTokens((s.current && s.current.filter) || {}, ctx))\n\t\t\t\tconst prevFilter = dropOptionalUnresolved(resolveFilterTokens((s.previous && s.previous.filter) || {}, ctx))\n\t\t\t\tconst [cur, prev] = await Promise.all([\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: curFilter }, ctx),\n\t\t\t\t\tfetchAggregateValue({ ...base, filter: prevFilter }, ctx),\n\t\t\t\t])\n\t\t\t\tthis.current = cur\n\t\t\t\tthis.previous = prev\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.current = null\n\t\t\t\tthis.previous = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card itself — its box, body stack, icon circle, title and value\n * typography — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget and CnStatsBlock. This tile is the one the canonical look was\n * modelled on, and it used to draw that look from its OWN copy of the rules\n * here: a 44px circle, a 16px gap, a 1.8em value. That copy is exactly how a\n * \"canonical\" card ends up with a second definition, so it is gone. What is\n * left is only what belongs to THIS widget: the delta chip and the error glyph.\n */\n\n/* The percentage-change chip beside the value. Tinted good/bad at runtime from\n * `goodDirection`, so the colour is an inline style, not a class. */\n.cn-delta-widget__delta {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n/* The dash shown when the number could not be fetched. It stands where the\n * value stands, so it takes the value's size and weight — only the colour\n * differs, because a failure is not a number. */\n.cn-delta-widget__error {\n\tfont-size: var(--cn-kpi-value-size, 2rem);\n\tfont-weight: var(--cn-kpi-value-weight, 700);\n\tline-height: var(--cn-kpi-value-line, 1.1);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n</style>\n"],"names":["_createBlock","_mergeProps","_createElementBlock","_createVNode","_Fragment","_openBlock","_resolveDynamicComponent","_createTextVNode"],"mappings":";;;;AA8BQ,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mDAAmD,GAAA;;;;AAerD,EAAA,KAAA,EAAA,6CAAA;;;;;;;uBApCTA,oCACI,IAAO,CAAA,OAAA,CAAA,EADXC;SAEK,GAAA,6BAA6B,EAAA;AAClC,GAAA,EAAA,wBAAA,EAAA,IAAA,CAAA,QAAA,6BAAA,IAAA,CAAA,QAAA,EAAA;qBAAA,QAAA,CAAA,UAAA;GAAA,QAAA,CAAA,IAAA,GAAA,mBAAA,GAAA,qBAAA;;KAKO,IAAS,CAAA,SAAA,CAAA,EAAA;qBAChB,MAKK;AAJD,MAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA;wBADJC,kBAKK,CAAA,KAAA,EAAA;;;AAFJ,YAAA,KAAK,iBAAE,QAAe,CAAA,eAAA,CAAA;;YACvBC,WAAgD,CAAA,uBAAA,EAAA;cAAjC,IAAI,EAAE,MAAO,CAAA,OAAA,CAAC,IAAI;AAAG,cAAA,IAAI,EAAE,EAAE;;;;;AAIjC,QAAA,CAAA,MAAA,CAAA,OAAO,CAAC,KAAK;0BAAxBD;;AAA0B,cAAA,KAAK,EAAC,2CAA8B;AAAA,cAAA,KAAA,EAAA,QAAA,CAAA,aAAA;+BAC1D,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;2BAgBZ,iBAAA,EAAA;WAZC,QAAA,CAAA,cAAA;uCAAA,CAAA,wBAAA,EAAA;;AAAA,gBAAA,IAAA,EAAA,EAAA;;eACY,QAAQ,CAAA,YAAA;8BAAvBA,kBAAuB,CAAA,MAAA,EAAA;;AAAA,kBAAA,KAAA,EAAA,wBAAA;AAAA,kBAAA,KAAA,EAAA,QAAA,CAAA,YAAA;mBAAA,GAAA,EAAA,CAAA,cAAA,UAAA,CAAA;gDAUd,CAAAE,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;oCARqB,CAAA,MAAA,EAAA;AAAzB,oBAAA,KAAK,EAAC,2CAAmB;AAAA,oBAAA,KAAA,iBAAA,QAAA,CAAA,UAAA,CAAA;qCAAA,QAAA,CAAA,gBAAA,CAAA,EAAA,CAAA,mBAAA;mBAExB,QAAQ,CAAA,QAAA,KAAA,IAAA;oCADdF,kBAMM,CAAA,MAAA,EAAA;;AAJN,wBAAA,KAAK,EAAA,wBAAA;AACJ,wBAAA,KAAK,0BAAE,QAAA,CAAA,UAAA,EAAA,CAAA;;AACR,yBAAAG,SAAA,EAAA,EAAAL,WAAA,CAAEM,uBAAA,CAAA,QAAA,CAAA,SAAA,CAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;AAAA,wBAAAC,eAAA,CAAA;;;;;AAMI,QAAA,CAAA,CAAA,QAAA,CAAA,cAAA,IAAA,CAAA,QAAA,CAAA,YAAA,IAAA,MAAA,CAAA,OAAA,CAAA,OAAA;0BAATL,0BAAA;;;;;;;;;;"}