@conduction/nextcloud-vue 2.111.0 → 2.113.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 (153) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +4 -6
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  6. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
  7. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
  9. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  14. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  22. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  23. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  24. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  29. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  34. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  35. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  37. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -3
  38. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  40. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -13
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -3
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  46. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  47. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  48. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  49. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +222 -15
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +24 -7
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  59. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  60. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  61. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  63. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  64. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  66. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  67. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  68. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  69. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  70. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  71. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  72. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  73. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  74. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +29 -12
  75. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  76. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +3 -1
  77. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  79. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  80. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  81. package/dist/esm/composables/useRefLabels.js +26 -6
  82. package/dist/esm/composables/useRefLabels.js.map +1 -1
  83. package/dist/esm/integrations/registry.js +4 -0
  84. package/dist/esm/integrations/registry.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +3 -1
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +3 -1
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/library-style.js +1 -1
  90. package/dist/esm/nextcloud-vue.css +680 -449
  91. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  92. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  93. package/dist/esm/store/plugins/logs.js +2 -1
  94. package/dist/esm/store/plugins/logs.js.map +1 -1
  95. package/dist/esm/utils/actionLink.js +104 -1
  96. package/dist/esm/utils/actionLink.js.map +1 -1
  97. package/dist/esm/utils/detailActionModel.js +28 -1
  98. package/dist/esm/utils/detailActionModel.js.map +1 -1
  99. package/dist/esm/utils/fillTemplate.js +13 -5
  100. package/dist/esm/utils/fillTemplate.js.map +1 -1
  101. package/dist/esm/utils/headerFieldChips.js +21 -2
  102. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  103. package/dist/esm/utils/mapList.js +26 -11
  104. package/dist/esm/utils/mapList.js.map +1 -1
  105. package/dist/esm/utils/savedViewMenu.js +12 -5
  106. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +2800 -1933
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +680 -449
  112. package/l10n/en.json +3 -1
  113. package/l10n/nl.json +3 -1
  114. package/package.json +1 -1
  115. package/src/components/CnActionButtons/CnActionButtons.vue +4 -6
  116. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  117. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  118. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  119. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  120. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  121. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  122. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  123. package/src/components/CnChoiceCards/CnChoiceCards.vue +0 -3
  124. package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -5
  125. package/src/components/CnDetailPage/CnDetailPage.vue +62 -14
  126. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  127. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  128. package/src/components/CnIndexPage/CnIndexPage.vue +233 -18
  129. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  130. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  131. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  132. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  133. package/src/components/CnReportsPage/CnReportsPage.vue +31 -12
  134. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  135. package/src/composables/useRefLabels.js +25 -5
  136. package/src/css/badge.css +15 -13
  137. package/src/css/board.css +0 -1
  138. package/src/css/dialog.css +53 -3
  139. package/src/css/index-page.css +26 -1
  140. package/src/css/look-board-detail.css +36 -15
  141. package/src/css/look-board-index.css +67 -15
  142. package/src/css/look-board.css +32 -18
  143. package/src/integrations/registry.js +4 -0
  144. package/src/schemas/app-manifest-v2.schema.json +61 -13
  145. package/src/store/plugins/liveUpdates.js +3 -1
  146. package/src/store/plugins/logs.js +2 -1
  147. package/src/utils/actionLink.js +103 -0
  148. package/src/utils/detailActionModel.js +27 -0
  149. package/src/utils/fillTemplate.js +13 -5
  150. package/src/utils/headerFieldChips.js +21 -2
  151. package/src/utils/mapList.js +26 -11
  152. package/src/utils/savedViewMenu.js +12 -5
  153. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -104,7 +104,11 @@
104
104
  :colorKey="pill.colorKey"
105
105
  :colorMap="pill.colorMap"
106
106
  :variant="pill.variant"
107
- :data-testid="`cn-detail-page-pill-${pill.key}`" />
107
+ :data-testid="`cn-detail-page-pill-${pill.key}`">
108
+ <template v-if="pill.icon" #icon>
109
+ <CnIcon :name="pill.icon" :size="14" class="cn-detail-page__pill-icon" />
110
+ </template>
111
+ </CnStatusBadge>
108
112
  </div>
109
113
  <!-- Type name as a small eyebrow above the record name, so
110
114
  the header reads "the record" not "the kind of record"
@@ -485,7 +489,11 @@
485
489
  :colorKey="pill.colorKey"
486
490
  :colorMap="pill.colorMap"
487
491
  :variant="pill.variant"
488
- :data-testid="`cn-detail-page-pill-${pill.key}`" />
492
+ :data-testid="`cn-detail-page-pill-${pill.key}`">
493
+ <template v-if="pill.icon" #icon>
494
+ <CnIcon :name="pill.icon" :size="14" class="cn-detail-page__pill-icon" />
495
+ </template>
496
+ </CnStatusBadge>
489
497
  </div>
490
498
  <CnBreadcrumbs
491
499
  v-if="breadcrumbCrumbs.length > 0"
@@ -1249,6 +1257,7 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
1249
1257
  import { gridLayout } from '../../mixins/gridLayout.js'
1250
1258
  import { pageViews } from '../../mixins/pageViews.js'
1251
1259
  import { useObjectStore } from '../../store/index.js'
1260
+ import { headerActionAsLink } from '../../utils/actionLink.js'
1252
1261
  import { isAppInstalled } from '../../utils/appInstalled.js'
1253
1262
  import { compactLayoutRows } from '../../utils/dashboardPlacement.js'
1254
1263
  import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js'
@@ -1257,6 +1266,7 @@ import {
1257
1266
  MAX_QUICK_ACTIONS,
1258
1267
  normalisePinnedAction,
1259
1268
  pillCountSource,
1269
+ pillLook,
1260
1270
  resolveCountPill,
1261
1271
  resolveNextStep,
1262
1272
  resolvePill,
@@ -2210,6 +2220,11 @@ export default {
2210
2220
  * `@objectId` / `@object.<field>` token + local-predicate resolution.
2211
2221
  * Empty (the default) renders nothing.
2212
2222
  *
2223
+ * An entry with `href` is a link, never dispatched: an absolute
2224
+ * `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written, or an instance path
2225
+ * (leading `/`) through `generateUrl()`, opening in place unless
2226
+ * `target` is `'_blank'`.
2227
+ *
2213
2228
  * The menu, not a row of buttons: that is what the manifest schema has
2214
2229
  * always said this key is, and what CnIndexPage already does with it.
2215
2230
  * The two surfaces had drifted, and on a dossiq case — twelve actions —
@@ -2497,9 +2512,12 @@ export default {
2497
2512
  * `field`: `text`, a constant ("Inwoner"), or `countFrom`
2498
2513
  * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`
2499
2514
  * with `{count}`), a count of a related list ("1 lopende zaak") that
2500
- * shows nothing at 0.
2515
+ * shows nothing at 0. With `resolve: { register, schema, labelField?,
2516
+ * colorField?, iconField? }` the pill shows the referenced record's
2517
+ * label, takes its tone from that record's colour (an NL Design System
2518
+ * hue such as `red`, or a badge role) and shows its icon.
2501
2519
  *
2502
- * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string}|null}
2520
+ * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object}|null}
2503
2521
  */
2504
2522
  typePill: {
2505
2523
  type: Object,
@@ -2940,6 +2958,8 @@ export default {
2940
2958
  pillCountSeq: 0,
2941
2959
  /** The labels of the records pills with `resolve` point at, by pill key. */
2942
2960
  pillRefLabels: {},
2961
+ /** The colour and icon names read off the records pills with `resolve` point at, by pill key. */
2962
+ pillRefLooks: {},
2943
2963
  /** Sequence of the last pill label lookup, so a late answer for another record is dropped. */
2944
2964
  pillRefSeq: 0,
2945
2965
  /** `register/schema/id` the read-state call was last made for, so it is sent once per load. */
@@ -3488,10 +3508,11 @@ export default {
3488
3508
  // `resolve`: the referenced record's label replaces the id, and
3489
3509
  // keys the colour too ("Woo-verzoek", not a uuid).
3490
3510
  const resolved = this.pillRefLabels[key]
3511
+ const look = pillLook(this.pillRefLooks[key])
3491
3512
  if (typeof resolved === 'string' && resolved !== '') {
3492
- return { ...pill, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
3513
+ return { ...pill, ...look, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
3493
3514
  }
3494
- return { ...pill, key, label: this.effectiveTranslate(pill.label) }
3515
+ return { ...pill, ...look, key, label: this.effectiveTranslate(pill.label) }
3495
3516
  })
3496
3517
  .filter(Boolean)
3497
3518
  },
@@ -3526,7 +3547,8 @@ export default {
3526
3547
  .filter(([, config]) => config && config.resolve && config.resolve.register && config.resolve.schema && typeof config.field === 'string')
3527
3548
  .map(([key, config]) => {
3528
3549
  const pill = resolvePill({ field: config.field }, this.resolvedObject)
3529
- return pill ? [key, config.resolve.register, config.resolve.schema, config.resolve.labelField || '', pill.colorKey] : null
3550
+ const { register, schema, labelField, colorField, iconField } = config.resolve
3551
+ return pill ? [key, register, schema, labelField || '', pill.colorKey, colorField || '', iconField || ''] : null
3530
3552
  })
3531
3553
  .filter(Boolean)
3532
3554
  },
@@ -3925,6 +3947,7 @@ export default {
3925
3947
  * in. Edit goes LAST so it reads after the app's own actions in the
3926
3948
  * Actions menu — the page's affordance yields to the app's.
3927
3949
  *
3950
+ * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
3928
3951
  * @return {Array<object>} The action descriptors.
3929
3952
  */
3930
3953
  effectiveHeaderActions() {
@@ -3933,12 +3956,16 @@ export default {
3933
3956
  // written out in place) join the list CnActionButtons dispatches,
3934
3957
  // so they get the same dialogs, toasts and `visibleWhen` gating as
3935
3958
  // any header action. An id that is already declared is not added
3936
- // twice. With nothing pinned inline this is `headerActions` itself.
3959
+ // twice. With nothing pinned inline and no `href` entry this is
3960
+ // `headerActions` itself.
3937
3961
  const known = new Set(authored.map((action) => action && action.id))
3938
3962
  const inline = [this.stagePrimary, ...this.pinnedQuickActions]
3939
3963
  .filter((pinned) => pinned && pinned.inline && !known.has(pinned.id))
3940
3964
  .map((pinned) => pinned.inline)
3941
- const declared = inline.length > 0 ? [...authored, ...inline] : authored
3965
+ const combined = inline.length > 0 ? [...authored, ...inline] : authored
3966
+ // An entry (or child) with `href` becomes a link CnActionButtons renders as an anchor.
3967
+ const linked = combined.map(headerActionAsLink)
3968
+ const declared = linked.some((action, index) => action !== combined[index]) ? linked : combined
3942
3969
  if (!(this.foldsEditIntoActions && this.canEditRecord)) {
3943
3970
  return declared
3944
3971
  }
@@ -5182,12 +5209,13 @@ export default {
5182
5209
  *
5183
5210
  * A stage's action is dispatched the way a header action is, through
5184
5211
  * the `run()` CnActionButtons bound for it, and announced to the host
5185
- * with its declaration. A link action is left to the browser. Without
5186
- * a stage action this is the page's `primaryAction`, announced as
5187
- * before.
5212
+ * with its declaration. A link action is left to the browser and
5213
+ * announces nothing. Without a stage action this is the page's
5214
+ * `primaryAction`, announced as before.
5188
5215
  *
5189
5216
  * @return {void}
5190
5217
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
5218
+ * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
5191
5219
  */
5192
5220
  onPrimaryActionClick() {
5193
5221
  const entry = this.stagePrimaryEntry
@@ -5195,6 +5223,9 @@ export default {
5195
5223
  this.$emit('primary-action', this.primaryAction)
5196
5224
  return
5197
5225
  }
5226
+ if (entry.href) {
5227
+ return
5228
+ }
5198
5229
  this.$emit('primary-action', this.headerActionsById[entry.id] || { id: entry.id, label: entry.label })
5199
5230
  this.onPinnedActionClick(entry)
5200
5231
  },
@@ -5340,6 +5371,7 @@ export default {
5340
5371
  const seq = ++this.pillRefSeq
5341
5372
  if (targets.length === 0) {
5342
5373
  this.pillRefLabels = {}
5374
+ this.pillRefLooks = {}
5343
5375
  return
5344
5376
  }
5345
5377
  if (!this._pillRefResolver) {
@@ -5352,14 +5384,30 @@ export default {
5352
5384
  })
5353
5385
  }
5354
5386
  const labels = {}
5355
- await Promise.all(targets.map(async ([key, register, schema, labelField, id]) => {
5356
- const found = await this._pillRefResolver.resolve(register, schema, [id], labelField).catch(() => ({}))
5387
+ const looks = {}
5388
+ const field = async (register, schema, id, name) => {
5389
+ if (!name) {
5390
+ return null
5391
+ }
5392
+ const found = await this._pillRefResolver.resolve(register, schema, [id], name, { strict: true }).catch(() => ({}))
5393
+ return found && typeof found[id] === 'string' ? found[id] : null
5394
+ }
5395
+ await Promise.all(targets.map(async ([key, register, schema, labelField, id, colorField, iconField]) => {
5396
+ const [found, color, icon] = await Promise.all([
5397
+ this._pillRefResolver.resolve(register, schema, [id], labelField).catch(() => ({})),
5398
+ field(register, schema, id, colorField),
5399
+ field(register, schema, id, iconField),
5400
+ ])
5357
5401
  if (found && typeof found[id] === 'string') {
5358
5402
  labels[key] = found[id]
5359
5403
  }
5404
+ if (color || icon) {
5405
+ looks[key] = { color, icon }
5406
+ }
5360
5407
  }))
5361
5408
  if (seq === this.pillRefSeq) {
5362
5409
  this.pillRefLabels = labels
5410
+ this.pillRefLooks = looks
5363
5411
  }
5364
5412
  },
5365
5413
 
@@ -147,6 +147,39 @@
147
147
  hidden
148
148
  data-testid="cn-detail-widget-props-pending" />
149
149
 
150
+ <!-- A mount-mode leaf whose descriptor asks for the library card
151
+ (`chrome: "card"`): the host draws the card, its title with the
152
+ count the leaf reports, the leaf's actions in the title row and the
153
+ library empty state. The leaf reports them through the `onHeader`
154
+ mount prop and renders only its rows. -->
155
+ <CnWidgetWrapper
156
+ v-else-if="wrapsMountLeaf"
157
+ :title="leafCardTitle"
158
+ titleIconPosition="left"
159
+ :showActions="false"
160
+ :showRefresh="false"
161
+ :showRequestFeature="false"
162
+ :headerLink="cardHeaderLink"
163
+ class="cn-detail-page__leaf-card"
164
+ data-testid="cn-detail-widget-leaf-card">
165
+ <template v-if="leafCardIcon" #title-icon>
166
+ <CnIcon :name="leafCardIcon" :size="20" />
167
+ </template>
168
+ <template v-if="rendererHeader.count !== null" #title-meta>
169
+ <span class="cn-detail-page__catalog-count" data-testid="cn-detail-widget-header-count">{{ rendererHeader.count }}</span>
170
+ </template>
171
+ <template v-if="hasRendererHeaderItems" #actions>
172
+ <CnDetailWidgetHeaderItems :header="rendererHeader" />
173
+ </template>
174
+ <CnWidgetEmptyState
175
+ v-if="rendererHeader.empty"
176
+ :name="rendererHeader.empty.name"
177
+ :description="rendererHeader.empty.description" />
178
+ <CnLeafMountHost
179
+ :provider="integrationProvider"
180
+ :mountProps="integrationMountProps" />
181
+ </CnWidgetWrapper>
182
+
150
183
  <CnLeafMountHost
151
184
  v-else-if="isMountIntegration"
152
185
  :provider="integrationProvider"
@@ -293,6 +326,7 @@ import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
293
326
  import CnObjectFilesWidget from '../CnObjectFilesWidget/CnObjectFilesWidget.vue'
294
327
  import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
295
328
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
329
+ import CnWidgetEmptyState from '../CnWidgetEmptyState/CnWidgetEmptyState.vue'
296
330
  import CnDetailWidgetHeaderItems from './CnDetailWidgetHeaderItems.vue'
297
331
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
298
332
  import { normalizeLook } from '../../composables/useLook.js'
@@ -373,6 +407,7 @@ export default {
373
407
  CnObjectDataWidget,
374
408
  CnObjectGeoWidget,
375
409
  CnRelatedObjectsWidget,
410
+ CnWidgetEmptyState,
376
411
  CnWidgetWrapper,
377
412
  NcActionButton,
378
413
  CnEmptyContent,
@@ -628,7 +663,7 @@ export default {
628
663
  * `header` event (screens-relation-pages): a link and a note,
629
664
  * with the list's total already filled in.
630
665
  */
631
- rendererHeader: { link: null, note: '', actions: [], pill: null },
666
+ rendererHeader: { link: null, note: '', actions: [], pill: null, count: null, empty: null },
632
667
  }
633
668
  },
634
669
 
@@ -844,6 +879,34 @@ export default {
844
879
  && typeof provider.unmount === 'function'
845
880
  },
846
881
 
882
+ /**
883
+ * Whether a mount-mode leaf gets the library card: on a card surface,
884
+ * grid cell included, when its descriptor asks for it (`chrome:
885
+ * "card"`). Opt-in, because a leaf that draws its own card would
886
+ * otherwise sit in two.
887
+ *
888
+ * @return {boolean}
889
+ * @spec openspec/changes/integration-leaf-card/specs/integration-leaf-card/spec.md#requirement-a-mount-mode-leaf-can-take-the-library-card
890
+ */
891
+ wrapsMountLeaf() {
892
+ return !this.isBare && this.isMountIntegration && this.integrationProvider.chrome === 'card'
893
+ },
894
+
895
+ /**
896
+ * The leaf card's title: the manifest's, else the descriptor's label.
897
+ *
898
+ * @return {string} The title in the user's language.
899
+ */
900
+ leafCardTitle() {
901
+ const title = widgetTitleOf(this.widget)
902
+ return title ? this.translateLabel(title) : (this.integrationProvider?.label || '')
903
+ },
904
+
905
+ /** @return {string} The leaf card's icon: the manifest's, else the descriptor's. */
906
+ leafCardIcon() {
907
+ return this.widget?.icon || this.integrationProvider?.icon || ''
908
+ },
909
+
847
910
  /**
848
911
  * Whether this render is using a provider's widget as its bare surface.
849
912
  *
@@ -1017,6 +1080,8 @@ export default {
1017
1080
  // one that reads `objectId`.
1018
1081
  integrationContext: this.widgetIntegrationContext,
1019
1082
  ...this.resolvedWidgetProps,
1083
+ // A carded leaf reports its title row and empty state here.
1084
+ ...(this.wrapsMountLeaf ? { onHeader: this.onRendererHeader } : {}),
1020
1085
  }
1021
1086
  },
1022
1087
 
@@ -1221,11 +1286,12 @@ export default {
1221
1286
 
1222
1287
  methods: {
1223
1288
  /**
1224
- * Take the header a content-only renderer, or an app widget in the
1225
- * titled card, reports.
1289
+ * Take the header a content-only renderer, an app widget in the
1290
+ * titled card, or a carded mount-mode leaf (`onHeader`) reports.
1226
1291
  *
1227
- * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null, actions?: Array<{label: string, icon?: string, run: () => unknown}>}} header The header parts.
1292
+ * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null, actions?: Array<{label: string, icon?: string, run: () => unknown}>, count?: number, empty?: {name: string, description?: string}|null}} header The header parts.
1228
1293
  * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
1294
+ * @spec openspec/changes/integration-leaf-card/specs/integration-leaf-card/spec.md#requirement-a-carded-leaf-reports-its-count-actions-and-empty-state
1229
1295
  */
1230
1296
  onRendererHeader(header) {
1231
1297
  const link = header && header.link && typeof header.link === 'object' ? header.link : null
@@ -1234,7 +1300,11 @@ export default {
1234
1300
  // `action` (one) and `actions` (several) both count, `action` first.
1235
1301
  const actions = [header && header.action, ...((header && Array.isArray(header.actions)) ? header.actions : [])].filter(usable)
1236
1302
  const pill = header && header.pill && typeof header.pill.label === 'string' && header.pill.label !== '' ? header.pill : null
1237
- this.rendererHeader = { link, note, actions, pill }
1303
+ const count = header && Number.isFinite(header.count) ? header.count : null
1304
+ const empty = header && header.empty && typeof header.empty.name === 'string' && header.empty.name !== ''
1305
+ ? { name: header.empty.name, description: typeof header.empty.description === 'string' ? header.empty.description : '' }
1306
+ : null
1307
+ this.rendererHeader = { link, note, actions, pill, count, empty }
1238
1308
  },
1239
1309
 
1240
1310
  /**
@@ -1372,6 +1442,31 @@ export default {
1372
1442
  white-space: nowrap;
1373
1443
  }
1374
1444
 
1445
+ /* A leaf card in a narrow cell keeps its title and count: the title row
1446
+ wraps and the actions move under it instead of squeezing the title out. */
1447
+ .cn-detail-page__leaf-card :deep(.cn-widget-wrapper__header) {
1448
+ flex-wrap: wrap;
1449
+ }
1450
+
1451
+ .cn-detail-page__leaf-card :deep(.cn-widget-wrapper__header-left) {
1452
+ flex: 1 0 auto;
1453
+ }
1454
+
1455
+ /* The count beside a card title: the grey 20px badge of the tab strip. */
1456
+ .cn-detail-page__catalog-count {
1457
+ background-color: var(--color-border);
1458
+ border-radius: 10px;
1459
+ box-sizing: border-box;
1460
+ color: var(--color-main-text);
1461
+ display: inline-block;
1462
+ font-size: 12px;
1463
+ font-weight: 600;
1464
+ line-height: 20px;
1465
+ min-width: 20px;
1466
+ padding: 0 6px;
1467
+ text-align: center;
1468
+ }
1469
+
1375
1470
  .cn-detail-page__catalog-note {
1376
1471
  color: var(--color-text-maxcontrast);
1377
1472
  font-size: 14px;
@@ -2,7 +2,7 @@
2
2
  <CnDialog
3
3
  :look="look"
4
4
  :width="width"
5
- defaultWidth="form"
5
+ :defaultWidth="columns === 2 ? 'wizard' : 'form'"
6
6
  :eyebrow="eyebrow"
7
7
  :subtitle="subtitle"
8
8
  :name="resolvedTitle"