@conduction/nextcloud-vue 2.83.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 (136) hide show
  1. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  6. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  7. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  8. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  10. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  11. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  17. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +5 -0
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  23. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  24. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  31. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  35. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  36. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  39. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  49. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  50. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  51. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  53. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  55. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  56. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  57. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  58. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
  61. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  64. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  65. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  67. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  73. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  74. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  75. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  76. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  77. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  78. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  79. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  80. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  81. package/dist/esm/l10n/en.json.js +16 -1
  82. package/dist/esm/l10n/en.json.js.map +1 -1
  83. package/dist/esm/l10n/nl.json.js +38 -1
  84. package/dist/esm/l10n/nl.json.js.map +1 -1
  85. package/dist/esm/library-style.js +1 -1
  86. package/dist/esm/nextcloud-vue.css +448 -296
  87. package/dist/esm/utils/buildManifest.js +37 -3
  88. package/dist/esm/utils/buildManifest.js.map +1 -1
  89. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  90. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  91. package/dist/esm/utils/formatMetric.js +39 -3
  92. package/dist/esm/utils/formatMetric.js.map +1 -1
  93. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  94. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  95. package/dist/esm/utils/sentinelTokens.js +9 -0
  96. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  97. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  98. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  99. package/dist/nextcloud-vue.cjs.js +1706 -454
  100. package/dist/nextcloud-vue.cjs.js.map +2 -2
  101. package/dist/nextcloud-vue.css +448 -296
  102. package/l10n/en.json +16 -1
  103. package/l10n/nl.json +38 -1
  104. package/package.json +1 -1
  105. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  106. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  107. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  108. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
  109. package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
  110. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  111. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  112. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  113. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  114. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  115. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  116. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  117. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
  118. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  119. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  120. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  121. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  122. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  123. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  124. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  125. package/src/css/form-field.css +14 -0
  126. package/src/css/form-widget.css +11 -0
  127. package/src/css/look-board-reports.css +58 -0
  128. package/src/css/look-board.css +12 -0
  129. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  130. package/src/schemas/app-manifest-v2.schema.json +23 -2
  131. package/src/utils/buildManifest.js +36 -2
  132. package/src/utils/detailWidgetVisibility.js +145 -0
  133. package/src/utils/formatMetric.js +38 -2
  134. package/src/utils/resolveManifestTokens.js +1 -0
  135. package/src/utils/sentinelTokens.js +9 -0
  136. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -2,7 +2,7 @@ import script from './CnAuditTrailCard.vue2.js';
2
2
  import { render } from './CnAuditTrailCard.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-b6134724";
5
+ script.__scopeId = "data-v-280a5ace";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -109,7 +109,7 @@ var script = {
109
109
  if (!this.register || !this.schema || !this.objectId) {
110
110
  return ''
111
111
  }
112
- return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`
112
+ return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`
113
113
  },
114
114
  },
115
115
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnAuditTrailCard.vue2.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnAuditTrailCard.vue2.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t"],"mappings":";;;;;;;AAoDA;;;AAGC;;AAEA;AACA;;AAEA;AACA;;AAEA;;AAED,aAAe;CACd,IAAI,EAAE,kBAAkB;;AAExB,CAAA,UAAU,EAAE,gBAAEA,UAAY,EAAE,eAAe;;AAE3C,CAAA,KAAK,EAAE;AACN;AACA,EAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACvC;AACA,EAAA,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACrC;AACA,EAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACvC;;;;;;AAMC;AACA;;AAED,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,QAAQ;GACjB,SAAS,EAAE,CAAC,KAAK,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;GACvD;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,aAAa;GACtB,SAAS,EAAE,CAAC,KAAK,KAAK,CAAC,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;GACzG;;AAED;EACA,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,wBAAuB,EAAG;;AAE5D,EAAA,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG;;AAExC,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO;;AAE9C,EAAA,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAC,EAAG;;AAEpC,EAAA,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMC,SAAC,CAAC,eAAe,EAAE,sBAAsB,GAAG;AAC3F;AACA,EAAA,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,UAAU,GAAG;AAC7E;AACA,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;EAC1E;;AAED,CAAA,KAAK,EAAE,CAAC,UAAU,CAAC;;AAEnB,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;GACN,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;GACzB,OAAO,EAAE,EAAE;GACX,OAAO,EAAE,KAAK;AACf,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT,EAAA,aAAa,GAAG;GACf,OAAO,IAAI,CAAC,KAAM,IAAGA,SAAC,CAAC,eAAe,EAAE,aAAa,CAAA;GACrD;;AAED,EAAA,gBAAgB,GAAG;AAClB,GAAA,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAA;GAC5C;;AAED;AACC;;;AAGA;AACA;AACA;;EAED,QAAQ,GAAG;AACV,GAAA,MAAM,MAAK,GAAI,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAA,EAAG,EAAA;GACrE,IAAI,IAAI,CAAC,KAAM,KAAI,KAAK,EAAE;AACzB,IAAA,IAAI,IAAI,CAAC,QAAQ,EAAE;AAClB,KAAA,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,QAAQ,EAAA;AACrC,KAAA;AACA,IAAA,IAAI,IAAI,CAAC,MAAM,EAAE;AAChB,KAAA,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAA;AACjC,KAAA;AACA,IAAA,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;AACnE,IAAA;GACA,IAAI,CAAC,IAAI,CAAC,QAAO,IAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE;AACrD,IAAA,OAAO,EAAC;AACT,IAAA;GACA,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;GAClH;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,QAAQ,EAAE;AACT,GAAA,SAAS,EAAE,IAAI;GACf,OAAO,CAAC,GAAG,EAAE;AACZ,IAAA,IAAI,GAAG,EAAE;AACR,KAAA,IAAI,CAAC,YAAY,GAAC;AACnB,KAAA;IACA;GACD;EACD;;AAED,CAAA,OAAO,EAAE;AACR;;;AAGC;AACA;;;AAGD,EAAA,MAAM,YAAY,GAAG;AACpB,GAAA,MAAM,MAAM,IAAI,CAAC,SAAO;AACxB,GAAA,IAAI,CAAC,GAAG,EAAE;IACT,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;AAClB,GAAA,IAAI;AACH,IAAA,MAAM,QAAO,GAAI,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,YAAY,IAAI,EAAA;AACxE,IAAA,IAAI,QAAQ,CAAC,EAAE,EAAE;KAChB,MAAM,IAAK,GAAE,MAAM,QAAQ,CAAC,IAAI,GAAC;AACjC,KAAA,MAAM,IAAG,GAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAK,IAAG,KAAG;AAC7C,KAAA,IAAI,CAAC,UAAU,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,IAAG,GAAI,GAAC;KAC5C,MAAK;AACN,KAAA,IAAI,CAAC,OAAQ,GAAE,GAAC;AACjB,KAAA;IACC,CAAA,OAAO,GAAG,EAAE;AACb;IACA,OAAO,CAAC,KAAK,CAAC,gDAAgD,EAAE,GAAG,EAAA;AACnE,IAAA,IAAI,CAAC,OAAQ,GAAE,GAAC;AACjB,IAAA,SAAU;AACT,IAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;AACpB,IAAA;GACA;;AAED,EAAA,WAAW,CAAC,KAAK,EAAE;AAClB,GAAA,OAAO,KAAK,CAAC,gBAAe,IAAK,KAAK,CAAC,KAAI,IAAK,KAAK,CAAC,MAAK,IAAK,KAAK,CAAC,IAAK,IAAG,EAAC;GAC/E;;AAED,EAAA,UAAU,CAAC,KAAK,EAAE;AACjB,GAAA,MAAM,GAAI,GAAE,KAAK,CAAC,gBAAe,IAAK,KAAK,CAAC,OAAQ,IAAG,KAAK,CAAC,UAAQ;AACrE,GAAA,IAAI,GAAI,KAAI,SAAU,IAAG,GAAE,KAAM,IAAI,EAAE;AACtC,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,MAAM,CAAA,GAAI,IAAI,IAAI,CAAC,GAAG,EAAA;AACtB,GAAA,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAE,KAAI,IAAI,EAAE;AACvC,IAAA,OAAO,MAAM,CAAC,GAAG,CAAA;AAClB,IAAA;GACA,OAAO,CAAC,CAAC,cAAc,EAAC;GACxB;EACD;AACF;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnAuditTrailCard.vue3.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue?vue&type=template&id=b6134724&lang.js"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;;;AA+BwC,EAAA,KAAA,EAAA,sBAAA;;;;AAGjC,EAAA,KAAA,EAAA,qBAAA;;AAKE,MAAA,UAAA,GAAA,EAAA,KAAE,EAAA,yBAAA,GAAA;AACC,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,uBAAA,GAAA;AACD,MAAA,UAAA,GAAA,EAAA,KAAI,EAAA,qBAAA,GAAA;;;;;;;;uBAhBXA;SACI,EAAE,QAAa,CAAA,aAAA;AACnB,IAAA,IAAG,EAAE,KAAO,CAAA,OAAA;AACX,IAAA,WAAS,EAAE,MAAW,CAAA,WAAA;kBACZ,EAAE,MAAO,CAAA,OAAA;;2BACf;OAAA,KAAA,CAAA,OAAA;mCAAA,CAAA,wBAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;AACS,UAAA,CAAA,KAAA,CAAA,OAAO,CAAC,MAAM,KAAA,CAAA;0BAA5BC,kBAEI,CAAA,KAAA,EAFJ,UAEI,EAAAC,eAAA,CADF,MAAc,CAAA,cAAA,CAAA,EAAA,CAAA,YAAA;;gCAGjBD,8CACiB,QAAgB,CAAA,gBAAA,EAAA,MAApB,KAAA;qCADbA;kBAEE,GAAG,EAAC,KAAM,CAAC,EAAE;AACd,kBAAA,KAAK,EAAC,oBAAoB;;AAC1B,kBAAAE,kBAAA,QAAA;oBACCA,kBAAO,CAAA,QAAA,EAAP,UAAO,EAAAD,eAAA,CAAA,KAAA,CAAA,MAAA,IAAA,KAAA,CAAA,KAAA,IAAA,MAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;AACP,oBAAAC,kBAAA,CAAU,MAAV,EAAA,UAAU,EAAAD,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnAuditTrailCard.vue3.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue?vue&type=template&id=280a5ace&lang.js"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;;;AA+BwC,EAAA,KAAA,EAAA,sBAAA;;;;AAGjC,EAAA,KAAA,EAAA,qBAAA;;AAKE,MAAA,UAAA,GAAA,EAAA,KAAE,EAAA,yBAAA,GAAA;AACC,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,uBAAA,GAAA;AACD,MAAA,UAAA,GAAA,EAAA,KAAI,EAAA,qBAAA,GAAA;;;;;;;;uBAhBXA;SACI,EAAE,QAAa,CAAA,aAAA;AACnB,IAAA,IAAG,EAAE,KAAO,CAAA,OAAA;AACX,IAAA,WAAS,EAAE,MAAW,CAAA,WAAA;kBACZ,EAAE,MAAO,CAAA,OAAA;;2BACf;OAAA,KAAA,CAAA,OAAA;mCAAA,CAAA,wBAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;AACS,UAAA,CAAA,KAAA,CAAA,OAAO,CAAC,MAAM,KAAA,CAAA;0BAA5BC,kBAEI,CAAA,KAAA,EAFJ,UAEI,EAAAC,eAAA,CADF,MAAc,CAAA,cAAA,CAAA,EAAA,CAAA,YAAA;;gCAGjBD,8CACiB,QAAgB,CAAA,gBAAA,EAAA,MAApB,KAAA;qCADbA;kBAEE,GAAG,EAAC,KAAM,CAAC,EAAE;AACd,kBAAA,KAAK,EAAC,oBAAoB;;AAC1B,kBAAAE,kBAAA,QAAA;oBACCA,kBAAO,CAAA,QAAA,EAAP,UAAO,EAAAD,eAAA,CAAA,KAAA,CAAA,MAAA,IAAA,KAAA,CAAA,KAAA,IAAA,MAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;AACP,oBAAAC,kBAAA,CAAU,MAAV,EAAA,UAAU,EAAAD,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnBodySections.vue2.js';
2
2
  import { render } from './CnBodySections.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-19a0caa4";
5
+ script.__scopeId = "data-v-44324dca";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -15,6 +15,8 @@ var script = {
15
15
  cnRegistry: { default: () => ({}) },
16
16
  /** Legacy custom-component map provided by CnAppRoot. */
17
17
  cnCustomComponents: { default: () => ({}) },
18
+ /** The host's translate function for manifest-authored strings, provided by CnAppRoot. */
19
+ cnTranslate: { default: null },
18
20
  },
19
21
 
20
22
  props: {
@@ -134,7 +136,7 @@ var script = {
134
136
  return {
135
137
  id,
136
138
  name,
137
- title: (section && section.title) || '',
139
+ title: this.translateTitle((section && section.title) || ''),
138
140
  card: !!(section && section.card),
139
141
  colSpan: section && section.colSpan,
140
142
  placement: section && section.placement,
@@ -146,6 +148,25 @@ var script = {
146
148
  },
147
149
 
148
150
  methods: {
151
+ /**
152
+ * A manifest-authored section title in the reader's language.
153
+ *
154
+ * The title is written in the manifest, so it belongs to the host
155
+ * app's catalogue, the same as every other manifest title the detail
156
+ * page shows. A section heading printed raw read "Asked about" to a
157
+ * Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).
158
+ *
159
+ * @param {string} title The title as written in the manifest.
160
+ * @return {string} The translated title, or '' when there is none.
161
+ * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
162
+ */
163
+ translateTitle(title) {
164
+ if (title === '' || typeof this.cnTranslate !== 'function') {
165
+ return title
166
+ }
167
+ return this.cnTranslate(title)
168
+ },
169
+
149
170
  /**
150
171
  * Resolve a section's host-app component by registry name. Precedence
151
172
  * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`
@@ -1 +1 @@
1
- {"version":3,"file":"CnBodySections.vue2.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: (section && section.title) || '',\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["t"],"mappings":";;;;;AAwEA,aAAe;AACd,CAAA,IAAI,EAAE,gBAAgB;;CAEtB,UAAU,EAAE;AACX,EAAA,iBAAiB;EACjB;;AAED,CAAA,MAAM,EAAE;;EAEP,UAAU,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA,EAAG;;AAEnC,EAAA,kBAAkB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG;EAC3C;;AAED,CAAA,KAAK,EAAE;AACN;;;;AAIC;;;AAGA;;;AAGD,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,KAAK;AACX,GAAA,OAAO,EAAE,MAAM,EAAE;GACjB;;AAED;;AAEC;;;;AAIA;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO,EAAE,CAAC;GACnB;;AAED;;;;;AAKC;AACA;;EAED,SAAS,EAAE;GACV,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;;;;;EAKA,IAAI,EAAE;GACL,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,8BAA8B,CAAC;GACjE;EACD;;CAED,KAAK,CAAC,KAAK,EAAE;;;;AAIZ;EACA,MAAM,gBAAe,GAAI,GAAG,CAAC,EAAE,IAAI,KAAK,CAAC,OAAM,IAAK,EAAE,CAAA,EAAG,EAAA;AACzD,EAAA,OAAO,CAAC,kBAAkB,EAAE,gBAAgB,EAAA;EAC5C,KAAK;AACJ,GAAA,MAAM,KAAK,CAAC,OAAO;AACnB,GAAA,CAAC,IAAI,KAAK;AACT,IAAA,gBAAgB,CAAC,KAAM,GAAE,EAAE,IAAI,IAAK,IAAG,EAAE,CAAE,GAAA;IAC3C;GACD,EAAE,IAAI,EAAE,IAAG,EAAG;AACf,IAAA;AACA,EAAA,OAAO,EAAC;EACR;;AAED,CAAA,QAAQ,EAAE;AACT;;;AAGC;AACA;;AAED,EAAA,OAAO,GAAG;AACT,GAAA,OAAO,IAAI,CAAC,IAAG,IAAK,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,YAAY,QAAQ,CAAA;GACnF;;AAED;;;AAGC;AACA;;AAED,EAAA,gBAAgB,GAAG;GAClB,MAAM,IAAG,GAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAE,GAAE,IAAI,CAAC,QAAS,GAAE,GAAC;GAC7D,IAAI,IAAI,CAAC,SAAQ,KAAM,IAAG,IAAK,IAAI,CAAC,SAAQ,KAAM,SAAS,EAAE;AAC5D,IAAA,OAAO,IAAG;AACX,IAAA;GACA,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAE,IAAG,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,CAAA;GAC/D;;AAED;;;;AAIC;AACA;AACA;;AAED,EAAA,gBAAgB,GAAG;GAClB,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,KAAK;AACpD,IAAA,MAAM,IAAK,GAAE,OAAM,IAAK,OAAO,CAAC,UAAQ;IACxC,MAAM,KAAK,CAAC,OAAM,IAAK,OAAO,CAAC,EAAE,KAAK,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAA;AAC/D,IAAA,OAAO;AACN,KAAA,EAAE;KACF,IAAI;AACJ,KAAA,KAAK,EAAE,CAAC,OAAM,IAAK,OAAO,CAAC,KAAK,KAAK,EAAE;AACvC,KAAA,IAAI,EAAE,CAAC,EAAE,OAAM,IAAK,OAAO,CAAC,IAAI,CAAC;AACjC,KAAA,OAAO,EAAE,OAAQ,IAAG,OAAO,CAAC,OAAO;AACnC,KAAA,SAAS,EAAE,OAAQ,IAAG,OAAO,CAAC,SAAS;AACvC,KAAA,SAAS,EAAE,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC;AAC7C,KAAA,KAAK,EAAE,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC;AACzC,KAAA;IACA,CAAA;GACD;EACD;;AAED,CAAA,OAAO,EAAE;AACR;;AAEC;;;AAGA;AACA;AACA;;;EAGD,uBAAuB,CAAC,IAAI,EAAE;AAC7B,GAAA,IAAI,OAAO,IAAG,KAAM,QAAO,IAAK,SAAS,EAAE,EAAE;AAC5C,IAAA,OAAO,IAAG;AACX,IAAA;GACA,MAAM,GAAI,GAAE,CAAC,IAAI,CAAC,UAAS,IAAK,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,KAAG;GAC7D,IAAI,OAAO,GAAG,CAAC,SAAS,EAAE;AACzB,IAAA,OAAO,GAAG,CAAC,SAAQ;AACpB,IAAA;AACA,GAAA,MAAM,MAAK,GAAI,IAAI,CAAC,kBAAmB,IAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAA;AACtE,GAAA,OAAO,MAAO,IAAG,IAAG;GACpB;;AAED;;;;;;;AAOC;AACA;;;EAGD,mBAAmB,CAAC,OAAO,EAAE;AAC5B,GAAA,MAAM,GAAI,GAAE,CAAC,WAAW,OAAO,CAAC,SAAS,OAAO,OAAO,CAAC,UAAU,QAAQ,IAAI,OAAO,CAAC,KAAI,GAAI,GAAC;GAC/F,MAAM,GAAE,GAAI,IAAI,CAAC,OAAM,IAAK,GAAC;GAC7B,MAAM,GAAE,GAAI,GAAC;AACb,GAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAE,IAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;AAC/C,IAAA,MAAM,WAAW,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAA;;;;IAI9C,IAAI,OAAO,QAAS,KAAI,YAAY,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA,KAAM,GAAG,EAAE;AAC/D,KAAA,QAAO;AACR,KAAA;AACA,IAAA,GAAG,CAAC,GAAG,CAAA,GAAI,SAAO;AACnB,IAAA;AACA,GAAA,OAAO,GAAE;GACT;;AAED;;;AAGC;AACA;AACA;;AAED,EAAA,YAAY,CAAC,KAAK,EAAE;GACnB,IAAI,CAAC,IAAI,CAAC,OAAQ,IAAG,OAAO,KAAK,CAAC,OAAM,KAAM,QAAQ,EAAE;AACvD,IAAA,OAAO,EAAC;AACT,IAAA;GACA,MAAM,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,EAAA;AACpD,GAAA,OAAO,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAE,EAAA;GACpC;;AAED;;AAEC;AACA;AACA;;EAED,eAAe,CAAC,KAAK,EAAE;GACtB,OAAOA,SAAC,CAAC,eAAe,EAAE,+CAA+C,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAG,IAAK,KAAK,CAAC,IAAI,CAAA;GAC3G;EACD;AACF;;;;"}
1
+ {"version":3,"file":"CnBodySections.vue2.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/** The host's translate function for manifest-authored strings, provided by CnAppRoot. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: this.translateTitle((section && section.title) || ''),\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest-authored section title in the reader's language.\n\t\t *\n\t\t * The title is written in the manifest, so it belongs to the host\n\t\t * app's catalogue, the same as every other manifest title the detail\n\t\t * page shows. A section heading printed raw read \"Asked about\" to a\n\t\t * Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).\n\t\t *\n\t\t * @param {string} title The title as written in the manifest.\n\t\t * @return {string} The translated title, or '' when there is none.\n\t\t * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language\n\t\t */\n\t\ttranslateTitle(title) {\n\t\t\tif (title === '' || typeof this.cnTranslate !== 'function') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn this.cnTranslate(title)\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnBodySections.vue3.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue?vue&type=template&id=19a0caa4&lang.js"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: (section && section.title) || '',\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_normalizeStyle"],"mappings":";;;;;;;;;AAkDQ,EAAA,OAAA,CAAA,QAAA,CAAA,gBAAK,CAAA,MAAA,GAAA,CAAA;;;QACR,KAAE,EAAAA,cAAA,CAAA,CAAC,kBAAiB,EAAA,EAAA,wBAAA,EACD,QAAA,CAAA,OAAA,EAAA,CAAA,CAAA;qBACX,EAAC,kBAAkB;;0BAC7BC,8CACgB,QAAgB,CAAA,gBAAA,EAAA,CAAzB,KAAK,KAAA;+BADZA;YAEC,GAAE,EAAA,KAAA,CAAA,EAAA;iBACE,EAACD,cAAA,CAAA,CAAA,2BAAA,EACyB,EAAA,iCAAA,EAAA,KAAA,CAAA,IAAA,EAAA,CAAA,CAAA;YAC9B,KAAK,EAAAE,cAAA,CAAE,QAAY,CAAA,YAAA,CAAC,KAAK,CAAA,CAAA;YACzB,iBAAe,EAAE,KAAK,CAAC,EAAC;AACzB,YAAA,aAAK,EAAA,iBAAA;;AAEF,YAAA,CAAA,KAAA,CAAA,KAAA;8BADHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnBodySections.vue3.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue?vue&type=template&id=44324dca&lang.js"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/** The host's translate function for manifest-authored strings, provided by CnAppRoot. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: this.translateTitle((section && section.title) || ''),\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest-authored section title in the reader's language.\n\t\t *\n\t\t * The title is written in the manifest, so it belongs to the host\n\t\t * app's catalogue, the same as every other manifest title the detail\n\t\t * page shows. A section heading printed raw read \"Asked about\" to a\n\t\t * Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).\n\t\t *\n\t\t * @param {string} title The title as written in the manifest.\n\t\t * @return {string} The translated title, or '' when there is none.\n\t\t * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language\n\t\t */\n\t\ttranslateTitle(title) {\n\t\t\tif (title === '' || typeof this.cnTranslate !== 'function') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn this.cnTranslate(title)\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_normalizeStyle"],"mappings":";;;;;;;;;AAkDQ,EAAA,OAAA,CAAA,QAAA,CAAA,gBAAK,CAAA,MAAA,GAAA,CAAA;;;QACR,KAAE,EAAAA,cAAA,CAAA,CAAC,kBAAiB,EAAA,EAAA,wBAAA,EACD,QAAA,CAAA,OAAA,EAAA,CAAA,CAAA;qBACX,EAAC,kBAAkB;;0BAC7BC,8CACgB,QAAgB,CAAA,gBAAA,EAAA,CAAzB,KAAK,KAAA;+BADZA;YAEC,GAAE,EAAA,KAAA,CAAA,EAAA;iBACE,EAACD,cAAA,CAAA,CAAA,2BAAA,EACyB,EAAA,iCAAA,EAAA,KAAA,CAAA,IAAA,EAAA,CAAA,CAAA;YAC9B,KAAK,EAAAE,cAAA,CAAE,QAAY,CAAA,YAAA,CAAC,KAAK,CAAA,CAAA;YACzB,iBAAe,EAAE,KAAK,CAAC,EAAC;AACzB,YAAA,aAAK,EAAA,iBAAA;;AAEF,YAAA,CAAA,KAAA,CAAA,KAAA;8BADHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnCountdownWidget.vue2.js';
2
2
  import { render } from './CnCountdownWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-17598258";
5
+ script.__scopeId = "data-v-5546e71c";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -43,6 +43,18 @@ const VARIANT_COLORS = Object.freeze({
43
43
  * negative number: "-3 days left" is a puzzle, and an overdue case is the one
44
44
  * state a handler must not have to decode.
45
45
  *
46
+ * ## A settled record
47
+ *
48
+ * `settledField` names a property on the record that is truthy once the work
49
+ * the date belongs to is done, such as a case in its final status. A settled
50
+ * record reads `settledLabel` (default "Closed") in no threshold colour, never
51
+ * "overdue": a closed case missed nothing that can still be acted on, and a red
52
+ * count of days on it sends a handler after a case that needs nobody.
53
+ *
54
+ * ```js
55
+ * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }
56
+ * ```
57
+ *
46
58
  * ## An unset date
47
59
  *
48
60
  * Renders a dash and no threshold colour. A case with no deadline is not urgent
@@ -65,8 +77,10 @@ var script = {
65
77
  * - `icon` — MDI icon name.
66
78
  * - `thresholds` — `{ warn, danger }` in days remaining.
67
79
  * - `showDate` — render the absolute date underneath (default true).
80
+ * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
81
+ * - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
68
82
  *
69
- * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}
83
+ * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
70
84
  */
71
85
  content: {
72
86
  type: Object,
@@ -89,6 +103,7 @@ var script = {
89
103
  return {
90
104
  cnObjectContextInjected: inject('cnObjectContext', null),
91
105
  cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
106
+ cnTranslate: inject('cnTranslate', null),
92
107
  }
93
108
  },
94
109
 
@@ -140,17 +155,44 @@ var script = {
140
155
  return Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)
141
156
  },
142
157
 
143
- /** @return {boolean} true once the date has passed. */
158
+ /**
159
+ * Whether the record says its work is done (`settledField` is truthy).
160
+ *
161
+ * @return {boolean} True for a settled record.
162
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
163
+ */
164
+ isSettled() {
165
+ const field = this.content?.settledField;
166
+ if (!this.record || !field) {
167
+ return false
168
+ }
169
+ return Boolean(getByPath(this.record, field))
170
+ },
171
+
172
+ /**
173
+ * Whether the date has passed on a record still open.
174
+ *
175
+ * @return {boolean} True when overdue.
176
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
177
+ */
144
178
  isOverdue() {
145
- return this.daysRemaining !== null && this.daysRemaining < 0
179
+ return !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0
146
180
  },
147
181
 
148
182
  /**
149
183
  * The headline: the remaining time in words.
150
184
  *
151
185
  * @return {string} The headline text.
186
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
152
187
  */
153
188
  headline() {
189
+ if (this.isSettled) {
190
+ const label = this.content?.settledLabel;
191
+ if (label) {
192
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label
193
+ }
194
+ return translate('nextcloud-vue', 'Closed')
195
+ }
154
196
  const days = this.daysRemaining;
155
197
  if (days === null) {
156
198
  return '—'
@@ -187,10 +229,11 @@ var script = {
187
229
  * The threshold band this date falls in.
188
230
  *
189
231
  * @return {string} One of default / success / warning / error.
232
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
190
233
  */
191
234
  variant() {
192
235
  const days = this.daysRemaining;
193
- if (days === null) {
236
+ if (days === null || this.isSettled) {
194
237
  return 'default'
195
238
  }
196
239
  if (days < 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t */\n\t\theadline() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=17598258&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t */\n\t\theadline() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
1
+ {"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=5546e71c&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDashboardGrid.vue2.js';
2
2
  import { render } from './CnDashboardGrid.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-29653a76";
5
+ script.__scopeId = "data-v-ca2ae49c";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };