@conduction/nextcloud-vue 2.82.0 → 2.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  13. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  14. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  17. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  18. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  21. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
  22. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +14 -0
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  30. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  31. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  42. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  43. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  50. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
  53. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
  55. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  57. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
  58. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
  61. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
  62. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
  64. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  70. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
  71. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  72. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
  73. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  74. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
  75. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
  76. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
  78. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  83. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  84. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +4 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  89. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
  90. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
  92. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
  94. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
  95. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
  97. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  99. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  106. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  107. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  108. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  109. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +16 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +41 -4
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +900 -537
  116. package/dist/esm/utils/buildManifest.js +37 -3
  117. package/dist/esm/utils/buildManifest.js.map +1 -1
  118. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  119. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  120. package/dist/esm/utils/fetchAggregate.js +25 -2
  121. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  122. package/dist/esm/utils/formatMetric.js +39 -3
  123. package/dist/esm/utils/formatMetric.js.map +1 -1
  124. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  125. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  126. package/dist/esm/utils/sentinelTokens.js +9 -0
  127. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  128. package/dist/esm/utils/switchValues.js +62 -0
  129. package/dist/esm/utils/switchValues.js.map +1 -0
  130. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  131. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  132. package/dist/nextcloud-vue.cjs.js +36711 -34909
  133. package/dist/nextcloud-vue.cjs.js.map +2 -2
  134. package/dist/nextcloud-vue.css +900 -537
  135. package/l10n/en.json +16 -1
  136. package/l10n/nl.json +41 -4
  137. package/package.json +1 -1
  138. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
  139. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  140. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  141. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  142. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
  143. package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
  144. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  145. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  146. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  147. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  148. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  149. package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
  150. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  151. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  152. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
  153. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
  154. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  155. package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
  156. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
  157. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  158. package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
  159. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
  160. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
  161. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  162. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  163. package/src/css/form-field.css +14 -0
  164. package/src/css/form-widget.css +11 -0
  165. package/src/css/index-page.css +11 -0
  166. package/src/css/index.css +1 -0
  167. package/src/css/look-board-reports.css +175 -0
  168. package/src/css/look-board.css +44 -1
  169. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  170. package/src/schemas/app-manifest-v2.schema.json +24 -3
  171. package/src/utils/buildManifest.js +36 -2
  172. package/src/utils/detailWidgetVisibility.js +145 -0
  173. package/src/utils/fetchAggregate.js +24 -1
  174. package/src/utils/formatMetric.js +38 -2
  175. package/src/utils/resolveManifestTokens.js +1 -0
  176. package/src/utils/sentinelTokens.js +9 -0
  177. package/src/utils/switchValues.js +59 -0
  178. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -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;;;;;;;;;;"}
@@ -46,6 +46,7 @@ import { isAppInstalled } from '../../utils/appInstalled.js';
46
46
  import { compactLayoutRows } from '../../utils/dashboardPlacement.js';
47
47
  import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js';
48
48
  import { stageOf, normalisePinnedAction, stageEntry, MAX_QUICK_ACTIONS, groupMenuEntries, resolveNextStep, pillCountSource, resolveCountPill, resolvePill } from '../../utils/detailActionModel.js';
49
+ import { visibilityContextRefs, widgetConditionData, fetchVisibilityRows, detailWidgetVisible } from '../../utils/detailWidgetVisibility.js';
49
50
  import { reportBindingProblems } from '../../utils/diagnostics.js';
50
51
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js';
51
52
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js';
@@ -53,6 +54,7 @@ import { resolveHeaderMeta, isActivityWidget } from '../../utils/headerMeta.js';
53
54
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js';
54
55
  import { setReadState } from '../../utils/recordInteractions.js';
55
56
  import { slotRenders } from '../../utils/slotContent.js';
57
+ import { isLocallyDecidableVisibleWhen, evaluateVisibleWhen } from '../../utils/visibleWhen.js';
56
58
  import { isDataWidgetDef, isRelatedWidgetDef, isGeoWidgetDef, isContentOnlyWidgetDef, isCardWidgetDef, widgetContentOf, resolveRegistryRenderer, widgetTitleOf } from '../../utils/widgetDispatch.js';
57
59
  import __sfc_main$a from '../CnActionButtons/CnActionButtons.vue.js';
58
60
  import __sfc_main$b from '../CnBreadcrumbs/CnBreadcrumbs.vue.js';
@@ -296,8 +298,8 @@ var script = {
296
298
 
297
299
  /**
298
300
  * Offer the board look's tab list name to the tabs widget below this page
299
- * (`config.tabsLabel`, default "<type> parts"). A getter object, so the
300
- * widget reads it live.
301
+ * (`config.tabsLabel`, default "<type> parts"), and the records
302
+ * `visibilityContext` names. Getter objects, so the widget reads them live.
301
303
  *
302
304
  * @return {object} The provided values.
303
305
  * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip
@@ -305,6 +307,7 @@ var script = {
305
307
  provide() {
306
308
  const isBoard = computed(() => this.isBoard);
307
309
  const tabsLabel = computed(() => this.resolvedTabsLabel);
310
+ const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows));
308
311
  return {
309
312
  // The page's own look reaches the tabs, the cards and the activity
310
313
  // below it, also when it came from the `look` prop and not the app.
@@ -314,6 +317,15 @@ var script = {
314
317
  return tabsLabel.value
315
318
  },
316
319
  },
320
+
321
+ // The records `visibilityContext` names, keyed `@context.<name>`, so a tab's
322
+ // `visibleWhen` reads the case type as a grid widget's does. Null
323
+ // while they are on their way.
324
+ cnDetailVisibilityRows: {
325
+ get value() {
326
+ return visibilityRows.value
327
+ },
328
+ },
317
329
  }
318
330
  },
319
331
 
@@ -1356,6 +1368,25 @@ var script = {
1356
1368
  default: '',
1357
1369
  },
1358
1370
 
1371
+ /**
1372
+ * Records the page reads beside the loaded one, so a `visibleWhen`
1373
+ * can ask about them (manifest `config.visibilityContext`). Each entry
1374
+ * is `{ <name>: { field, register, schema } }`: `field` is the
1375
+ * property of the loaded record holding the other record's id, and a
1376
+ * condition reads that record as `@context.<name>.<path>`. A case page that
1377
+ * declares `{ caseType: { field: "caseType", register: "dossiq",
1378
+ * schema: "caseType" } }` can show a widget for some case types only
1379
+ * with `{ field: "@context.caseType.category", op: "in", value: [...] }`.
1380
+ * A record that cannot be read is absent, so its condition fails safe.
1381
+ * Empty (the default) reads nothing extra.
1382
+ *
1383
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
1384
+ */
1385
+ visibilityContext: {
1386
+ type: Object,
1387
+ default: () => ({}),
1388
+ },
1389
+
1359
1390
  /**
1360
1391
  * Whether the current user administers this instance. Shows the
1361
1392
  * `adminOnly` header actions. `null` (the default) reads it from
@@ -1583,6 +1614,19 @@ var script = {
1583
1614
 
1584
1615
  data() {
1585
1616
  return {
1617
+ /**
1618
+ * The records `visibilityContext` names, keyed `@context.<name>`: the row,
1619
+ * `null` when it could not be read, absent while it is on its way.
1620
+ */
1621
+ visibilityRows: {},
1622
+ /** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */
1623
+ visibilityRowsFor: '',
1624
+ /** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */
1625
+ visibilityRowsSeq: 0,
1626
+ /** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */
1627
+ widgetConditionAnswers: {},
1628
+ /** Sequence of the last remote condition round. */
1629
+ widgetConditionSeq: 0,
1586
1630
  /** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
1587
1631
  pillCounts: {},
1588
1632
  /** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
@@ -1842,11 +1886,17 @@ var script = {
1842
1886
  * because a keyboard user who cannot see the page needs to know what
1843
1887
  * they are being offered before they take it.
1844
1888
  *
1889
+ * One library string with a placeholder, not "Skip to" from the host's
1890
+ * catalogue glued to the label: no app carried that fragment, so every
1891
+ * Dutch reader got "Skip to In behandeling nemen" (Zuiddrecht live
1892
+ * round 3), and a fragment cannot be put in Dutch word order anyway.
1893
+ *
1845
1894
  * @return {string} The link text.
1846
1895
  * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
1896
+ * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
1847
1897
  */
1848
1898
  skipLinkLabel() {
1849
- return `${this.effectiveTranslate('Skip to')} ${this.effectivePrimaryLabel}`
1899
+ return translate('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })
1850
1900
  },
1851
1901
 
1852
1902
  // ── The action model (detail-action-model-and-case-surfaces) ────
@@ -2688,6 +2738,68 @@ var script = {
2688
2738
  return store.objects?.[this.objectType]?.[this.objectId] ?? null
2689
2739
  },
2690
2740
 
2741
+ /**
2742
+ * The records `visibilityContext` names for the loaded record.
2743
+ *
2744
+ * @return {Array<{key: string, register: string, schema: string, id: string}>}
2745
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
2746
+ */
2747
+ visibilityRefs() {
2748
+ return visibilityContextRefs(this.visibilityContext, this.resolvedObject)
2749
+ },
2750
+
2751
+ /**
2752
+ * Whether a record `visibilityContext` names is still on its way.
2753
+ *
2754
+ * @return {boolean}
2755
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
2756
+ */
2757
+ visibilityRowsPending() {
2758
+ return this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey
2759
+ },
2760
+
2761
+ /**
2762
+ * The named references as one key, so another record (or another case
2763
+ * type on the same record) reads them again.
2764
+ *
2765
+ * @return {string}
2766
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
2767
+ */
2768
+ visibilityRefsKey() {
2769
+ return JSON.stringify(this.visibilityRefs)
2770
+ },
2771
+
2772
+ /**
2773
+ * What a grid widget's `visibleWhen` reads: the record, with the
2774
+ * `visibilityContext` records under `@context.<name>`. Null until the record loads.
2775
+ *
2776
+ * @return {object|null}
2777
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
2778
+ */
2779
+ widgetVisibilityData() {
2780
+ return widgetConditionData(this.resolvedObject, this.visibilityRows)
2781
+ },
2782
+
2783
+ /**
2784
+ * The grid widgets whose `visibleWhen` needs a request (endpoint or
2785
+ * source), with what they ask, as one key: a change re-asks them.
2786
+ *
2787
+ * @return {string}
2788
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
2789
+ */
2790
+ remoteWidgetConditionsKey() {
2791
+ if (!this.widgetVisibilityData || this.visibilityRowsPending) {
2792
+ return ''
2793
+ }
2794
+ const defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || [])];
2795
+ const remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'
2796
+ && !isLocallyDecidableVisibleWhen(def.visibleWhen));
2797
+ if (remote.length === 0) {
2798
+ return ''
2799
+ }
2800
+ return JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])
2801
+ },
2802
+
2691
2803
  /**
2692
2804
  * The `@self` block of the resolved object (interaction markers live here).
2693
2805
  *
@@ -3220,6 +3332,22 @@ var script = {
3220
3332
  },
3221
3333
 
3222
3334
  watch: {
3335
+ // The records a condition names are read again for another record.
3336
+ visibilityRefsKey: {
3337
+ immediate: true,
3338
+ handler() {
3339
+ this.refreshVisibilityRows();
3340
+ },
3341
+ },
3342
+
3343
+ // Endpoint and source conditions on grid widgets are asked once per record.
3344
+ remoteWidgetConditionsKey: {
3345
+ immediate: true,
3346
+ handler() {
3347
+ this.refreshWidgetConditions();
3348
+ },
3349
+ },
3350
+
3223
3351
  // The header's count pills count again for another record or other lists.
3224
3352
  pillCountKey: {
3225
3353
  immediate: true,
@@ -4458,6 +4586,62 @@ var script = {
4458
4586
  || undefined
4459
4587
  },
4460
4588
 
4589
+ /**
4590
+ * Read the records `visibilityContext` names for the loaded record. A
4591
+ * late answer for a record the page has left is dropped.
4592
+ *
4593
+ * @return {Promise<void>}
4594
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
4595
+ */
4596
+ async refreshVisibilityRows() {
4597
+ const key = this.visibilityRefsKey;
4598
+ const refs = this.visibilityRefs;
4599
+ const seq = ++this.visibilityRowsSeq;
4600
+ if (refs.length === 0) {
4601
+ // Write only on a change, so a page without `visibilityContext`
4602
+ // never touches the state its layout could read.
4603
+ if (Object.keys(this.visibilityRows).length > 0) {
4604
+ this.visibilityRows = {};
4605
+ }
4606
+ if (this.visibilityRowsFor !== key) {
4607
+ this.visibilityRowsFor = key;
4608
+ }
4609
+ return
4610
+ }
4611
+ if (!this._visibilityRowCache) {
4612
+ this._visibilityRowCache = new Map();
4613
+ }
4614
+ const rows = await fetchVisibilityRows(refs, this._visibilityRowCache);
4615
+ if (seq !== this.visibilityRowsSeq) {
4616
+ return
4617
+ }
4618
+ this.visibilityRows = rows;
4619
+ this.visibilityRowsFor = key;
4620
+ },
4621
+
4622
+ /**
4623
+ * Ask the endpoint and source `visibleWhen` conditions of the grid
4624
+ * widgets, once per record. Each is fail-safe: an error hides the widget.
4625
+ *
4626
+ * @return {Promise<void>}
4627
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
4628
+ */
4629
+ async refreshWidgetConditions() {
4630
+ const seq = ++this.widgetConditionSeq;
4631
+ if (this.remoteWidgetConditionsKey === '') {
4632
+ return
4633
+ }
4634
+ const [, entries] = JSON.parse(this.remoteWidgetConditionsKey);
4635
+ const ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined };
4636
+ const answers = {};
4637
+ await Promise.all(entries.map(async ([id, cond]) => {
4638
+ answers[id] = await evaluateVisibleWhen(cond, ctx);
4639
+ }));
4640
+ if (seq === this.widgetConditionSeq) {
4641
+ this.widgetConditionAnswers = answers;
4642
+ }
4643
+ },
4644
+
4461
4645
  /**
4462
4646
  * Drop the items that draw nothing, then close the rows they freed —
4463
4647
  * `float: true` leaves the item below pinned under a gap nobody owns.
@@ -4480,7 +4664,10 @@ var script = {
4480
4664
  * Whether a layout item has anything to draw, so a grid cell is worth
4481
4665
  * spending on it.
4482
4666
  *
4483
- * TWO widget types are judged, and no others.
4667
+ * A widget whose definition carries `visibleWhen` draws only while the
4668
+ * condition holds for the record (see `detailWidgetVisible`).
4669
+ *
4670
+ * Beyond that, TWO widget types are judged, and no others.
4484
4671
  *
4485
4672
  * `integration`, in the case where nothing can ever appear: a leaf from
4486
4673
  * another app that is not registered, with no `requiredApp` to fall back
@@ -4527,6 +4714,18 @@ var script = {
4527
4714
  if (!widget) {
4528
4715
  return true
4529
4716
  }
4717
+ // The widget's own `visibleWhen`, read against the record and the
4718
+ // records `visibilityContext` names (a case's case type). Without
4719
+ // one the widget shows, as it always did, and the layout takes no
4720
+ // dependency on the record: a page without conditions re-lays out
4721
+ // exactly as often as before.
4722
+ if (widget.visibleWhen !== undefined && widget.visibleWhen !== null
4723
+ && !detailWidgetVisible(widget, this.widgetVisibilityData, {
4724
+ pending: this.visibilityRowsPending,
4725
+ answers: this.widgetConditionAnswers,
4726
+ })) {
4727
+ return false
4728
+ }
4530
4729
  // The board look shows the activity as the History tab. When a tabs
4531
4730
  // widget already holds it, it is not also a body section.
4532
4731
  if (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {