@conduction/nextcloud-vue 2.111.0 → 2.112.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 (127) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
  8. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  11. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  17. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  18. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  21. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  26. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  27. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +32 -8
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -3
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  35. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  39. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +166 -2
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +19 -4
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  47. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  48. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  49. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  50. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  51. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  52. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  54. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  56. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  57. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  58. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  59. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  60. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  61. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  62. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  63. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  64. package/dist/esm/composables/useRefLabels.js +26 -6
  65. package/dist/esm/composables/useRefLabels.js.map +1 -1
  66. package/dist/esm/integrations/registry.js +4 -0
  67. package/dist/esm/integrations/registry.js.map +1 -1
  68. package/dist/esm/l10n/en.json.js +3 -1
  69. package/dist/esm/l10n/en.json.js.map +1 -1
  70. package/dist/esm/l10n/nl.json.js +3 -1
  71. package/dist/esm/l10n/nl.json.js.map +1 -1
  72. package/dist/esm/library-style.js +1 -1
  73. package/dist/esm/nextcloud-vue.css +533 -273
  74. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  75. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  76. package/dist/esm/store/plugins/logs.js +2 -1
  77. package/dist/esm/store/plugins/logs.js.map +1 -1
  78. package/dist/esm/utils/detailActionModel.js +28 -1
  79. package/dist/esm/utils/detailActionModel.js.map +1 -1
  80. package/dist/esm/utils/fillTemplate.js +13 -5
  81. package/dist/esm/utils/fillTemplate.js.map +1 -1
  82. package/dist/esm/utils/headerFieldChips.js +21 -2
  83. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  84. package/dist/esm/utils/mapList.js +26 -11
  85. package/dist/esm/utils/mapList.js.map +1 -1
  86. package/dist/esm/utils/savedViewMenu.js +12 -5
  87. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  88. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  89. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  90. package/dist/nextcloud-vue.cjs.js +2562 -1886
  91. package/dist/nextcloud-vue.cjs.js.map +2 -2
  92. package/dist/nextcloud-vue.css +533 -273
  93. package/l10n/en.json +3 -1
  94. package/l10n/nl.json +3 -1
  95. package/package.json +1 -1
  96. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  97. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  98. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  99. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  100. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  101. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  102. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  103. package/src/components/CnDetailPage/CnDetailPage.vue +42 -9
  104. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  105. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  106. package/src/components/CnIndexPage/CnIndexPage.vue +174 -4
  107. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  108. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  109. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  110. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  111. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  112. package/src/composables/useRefLabels.js +25 -5
  113. package/src/css/dialog.css +53 -3
  114. package/src/css/index-page.css +26 -1
  115. package/src/css/look-board-detail.css +36 -15
  116. package/src/css/look-board-index.css +69 -3
  117. package/src/css/look-board.css +30 -2
  118. package/src/integrations/registry.js +4 -0
  119. package/src/schemas/app-manifest-v2.schema.json +11 -5
  120. package/src/store/plugins/liveUpdates.js +3 -1
  121. package/src/store/plugins/logs.js +2 -1
  122. package/src/utils/detailActionModel.js +27 -0
  123. package/src/utils/fillTemplate.js +13 -5
  124. package/src/utils/headerFieldChips.js +21 -2
  125. package/src/utils/mapList.js +26 -11
  126. package/src/utils/savedViewMenu.js +12 -5
  127. package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json CHANGED
@@ -1912,7 +1912,9 @@
1912
1912
  "New team": "New team",
1913
1913
  "Why it is moving": "Why it is moving",
1914
1914
  "Use \"{name}\"": "Use \"{name}\"",
1915
- "It was not handed over.": "It was not handed over."
1915
+ "It was not handed over.": "It was not handed over.",
1916
+ "This search could not be read.": "This search could not be read.",
1917
+ "{source} could not be searched, so this list may be incomplete.": "{source} could not be searched, so this list may be incomplete."
1916
1918
  },
1917
1919
  "plurals": {
1918
1920
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2124,7 +2124,9 @@
2124
2124
  "New team": "Nieuw team",
2125
2125
  "Why it is moving": "Waarom het overgaat",
2126
2126
  "Use \"{name}\"": "\"{name}\" gebruiken",
2127
- "It was not handed over.": "Het is niet overgedragen."
2127
+ "It was not handed over.": "Het is niet overgedragen.",
2128
+ "This search could not be read.": "Deze zoekopdracht kon niet worden gelezen.",
2129
+ "{source} could not be searched, so this list may be incomplete.": "{source} kon niet worden doorzocht, dus deze lijst is mogelijk niet compleet."
2128
2130
  },
2129
2131
  "plurals": {
2130
2132
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.111.0",
3
+ "version": "2.112.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  {{ section.title }}
32
32
  </h3>
33
33
  <dl class="cn-action-dialog__facts">
34
- <div v-for="fact in section.facts" :key="fact.label">
34
+ <div v-for="fact in section.facts" :key="fact.label" class="cn-action-dialog__fact">
35
35
  <dt>{{ fact.label }}</dt>
36
36
  <dd>{{ fact.value }}</dd>
37
37
  </div>
@@ -87,6 +87,7 @@
87
87
  v-model="reason"
88
88
  :label="reasonLabel"
89
89
  :labelOutside="false"
90
+ :rows="config.reason.rows || 2"
90
91
  resize="vertical"
91
92
  data-testid="cn-action-dialog-reason" />
92
93
  <span v-if="config.reason.hint" class="cn-action-dialog__muted">{{ text(config.reason.hint) }}</span>
@@ -176,8 +177,9 @@ let reasonSeq = 0
176
177
  * required?, options?: [{ value, label, fills? }] }]`: inputs sent beside
177
178
  * the reason, required by default; a select option's `fills` sets other
178
179
  * fields that are still empty.
179
- * - `reason`: `{ label?, hint?, required?, field? }`: a text area; required by
180
- * default; sent as `field` (default `reason`).
180
+ * - `reason`: `{ label?, hint?, required?, field?, rows? }`: a text area; required by
181
+ * default; sent as `field` (default `reason`). `rows` is its height in lines
182
+ * (default 2; board DqBesluit draws 4).
181
183
  * - `note`: `{ variant?, title?, text }`: a read-only note.
182
184
  * - `confirm`: `{ label, icon?, request?: { method?, url, body? }, action? }`.
183
185
  * `request` posts itself (`body` templates filled with the record and
@@ -604,17 +606,27 @@ export default {
604
606
 
605
607
  .cn-action-dialog__title {
606
608
  margin: 0 0 8px;
607
- font-size: 15px;
608
- font-weight: 700;
609
+ font-size: 14px;
610
+ font-weight: 600;
609
611
  }
610
612
 
613
+ /* Facts in columns of at least 200px, each a label over its value above a
614
+ hairline (DqArchiveren "Bewaring volgens het zaaktype"). */
611
615
  .cn-action-dialog__facts {
612
616
  display: grid;
613
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
614
- gap: 8px 16px;
617
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
618
+ gap: 12px 24px;
615
619
  margin: 0;
616
620
  }
617
621
 
622
+ .cn-action-dialog__fact {
623
+ display: flex;
624
+ flex-direction: column;
625
+ gap: 3px;
626
+ padding-bottom: 10px;
627
+ border-bottom: 1px solid var(--color-border);
628
+ }
629
+
618
630
  .cn-action-dialog__facts dt {
619
631
  color: var(--color-text-maxcontrast);
620
632
  font-size: 13px;
@@ -622,12 +634,15 @@ export default {
622
634
 
623
635
  .cn-action-dialog__facts dd {
624
636
  margin: 0;
637
+ font-size: 15px;
638
+ font-weight: 500;
625
639
  }
626
640
 
641
+ /* Checklist rows divided by hairlines, the detail at the row's end
642
+ (DqArchiveren "Klaar om te archiveren"). */
627
643
  .cn-action-dialog__checks {
628
644
  display: flex;
629
645
  flex-direction: column;
630
- gap: 8px;
631
646
  margin: 0;
632
647
  padding: 0;
633
648
  list-style: none;
@@ -637,7 +652,15 @@ export default {
637
652
  display: flex;
638
653
  flex-wrap: wrap;
639
654
  align-items: center;
640
- gap: 8px;
655
+ gap: 2px 12px;
656
+ padding: 10px 0;
657
+ border-top: 1px solid var(--color-border);
658
+ font-size: 15px;
659
+ }
660
+
661
+ .cn-action-dialog__check-label {
662
+ flex: 1 1 auto;
663
+ font-weight: 600;
641
664
  }
642
665
 
643
666
  .cn-action-dialog__check-icon {
@@ -34,6 +34,7 @@
34
34
  v-model="note"
35
35
  :label="noteLabel"
36
36
  :labelOutside="false"
37
+ :rows="noteConfig.rows || 3"
37
38
  resize="vertical"
38
39
  data-testid="cn-assign-dialog-note" />
39
40
  <span v-if="noteConfig.hint" class="cn-assign-dialog__hint">{{ text(noteConfig.hint) }}</span>
@@ -139,8 +140,9 @@ export function withTypedAssignee(found, typed, label) {
139
140
  * by `currentAs`) says so, in place of `hint`; `allowTyped`
140
141
  * offers what was typed as a choice; the id is sent as `field` (default
141
142
  * `assignee`) and its kind as `kindField` when set.
142
- * - `note`: `{ label?, hint?, required?, field? }`, required by default and
143
- * sent as `field` (default `note`); `false` drops it.
143
+ * - `note`: `{ label?, hint?, required?, field?, rows? }`, required by default and
144
+ * sent as `field` (default `note`); `false` drops it. `rows` is the text
145
+ * area's height in lines (default 3, as board DqOverdragen draws it).
144
146
  * - `toggles`: `[{ field, label, note? }]`: switches that start off and are
145
147
  * sent as booleans.
146
148
  * - `confirm`: `{ label?, icon?, request?: { method?, url, body? }, action? }`.
@@ -228,7 +228,11 @@
228
228
  class="cn-board-view__pill"
229
229
  data-testid="cn-board-card-pill"
230
230
  :label="roleText(card, roles.pill)"
231
- :variant="pillVariant(card)" />
231
+ :variant="pillVariant(card)">
232
+ <template v-if="pillIcon(card)" #icon>
233
+ <CnIcon :name="pillIcon(card)" :size="14" />
234
+ </template>
235
+ </CnStatusBadge>
232
236
  </span>
233
237
  <span
234
238
  v-if="dueText(card) !== '' || ownerName(card) !== ''"
@@ -352,12 +356,14 @@
352
356
 
353
357
  <script>
354
358
  import { translate as t } from '@nextcloud/l10n'
359
+ import CnIcon from '../CnIcon/CnIcon.vue'
355
360
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
356
361
  import { normalizeLook } from '../../composables/useLook.js'
357
362
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
358
363
  import { buildBoardColumns, normalizeAllowed, resolveStageColor } from '../../utils/boardColumns.js'
359
364
  import { buildSwimlanes } from '../../utils/boardSwimlanes.js'
360
365
  import { DROP_OUTCOMES, runBoardDrop } from '../../utils/boardTransition.js'
366
+ import { pillLook } from '../../utils/detailActionModel.js'
361
367
  import { daysUntil, dueStateForRow } from '../../utils/dueRule.js'
362
368
  import { formatMetricValue } from '../../utils/formatMetric.js'
363
369
  import { readPath } from '../../utils/readPath.js'
@@ -393,7 +399,7 @@ import '../../css/board.css'
393
399
  export default {
394
400
  name: 'CnBoardView',
395
401
 
396
- components: { CnStatusBadge },
402
+ components: { CnIcon, CnStatusBadge },
397
403
 
398
404
  inject: {
399
405
  /**
@@ -489,9 +495,10 @@ export default {
489
495
 
490
496
  /**
491
497
  * The board look's card roles (manifest `config.board.card`):
492
- * `{ title, sub, pill, due, owner, pillColors? }`. `sub` is a field or
493
- * a list of fields joined by a middle dot; `pillColors` maps a pill
494
- * value to a badge variant. Missing, the first of `cardFields` is the
498
+ * `{ title, sub, pill, due, owner, pillColors?, resolve? }`. `sub` is a
499
+ * field or a list of fields joined by a middle dot; `pillColors` maps a
500
+ * pill value to a badge variant; `resolve: { colorField?, iconField? }`
501
+ * takes the pill's tone and icon from the record it points at. Missing, the first of `cardFields` is the
495
502
  * title and the rest form the sub line. The Nextcloud look ignores it.
496
503
  *
497
504
  * @type {{title?: string, sub?: string|Array<string>, pill?: string, due?: string, owner?: string, pillColors?: object}|null}
@@ -502,6 +509,20 @@ export default {
502
509
  default: null,
503
510
  },
504
511
 
512
+ /**
513
+ * The referenced records' colour and icon names, by field and id
514
+ * (`{ [field]: { [id]: { color, icon } } }`, CnIndexPage reads them).
515
+ * With `cardRoles.resolve: { colorField?, iconField? }` the card's pill
516
+ * takes its record's tone and icon from here.
517
+ *
518
+ * @type {object}
519
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-card-pill-takes-its-tone-and-icon-from-the-referenced-record
520
+ */
521
+ pillLooks: {
522
+ type: Object,
523
+ default: () => ({}),
524
+ },
525
+
505
526
  /**
506
527
  * The field on a column's state that holds its dot colour (manifest
507
528
  * `config.board.colorField`). Empty, the lifecycle state's declared
@@ -1064,11 +1085,43 @@ export default {
1064
1085
  * @return {string} A CnStatusBadge variant.
1065
1086
  */
1066
1087
  pillVariant(card) {
1088
+ const look = this.pillLookOf(card)
1089
+ if (look.variant) {
1090
+ return look.variant
1091
+ }
1067
1092
  const raw = this.fieldText(card, this.roles.pill)
1068
1093
  const variant = this.roles.pillColors[raw] ?? this.roles.pillColors[raw.toLowerCase()]
1069
1094
  return isBadgeVariant(variant) ? variant : 'default'
1070
1095
  },
1071
1096
 
1097
+ /**
1098
+ * The pill's icon, from the record it points at (`cardRoles.resolve`).
1099
+ *
1100
+ * @param {object} card The card's row.
1101
+ * @return {string} An icon name, or ''.
1102
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-card-pill-takes-its-tone-and-icon-from-the-referenced-record
1103
+ */
1104
+ pillIcon(card) {
1105
+ return this.pillLookOf(card).icon || ''
1106
+ },
1107
+
1108
+ /**
1109
+ * The tone and icon of the record the card's pill points at, when
1110
+ * `cardRoles.resolve` asks for them; empty otherwise.
1111
+ *
1112
+ * @param {object} card The card's row.
1113
+ * @return {{variant?: string, icon?: string}} The look.
1114
+ */
1115
+ pillLookOf(card) {
1116
+ const given = this.cardRoles && typeof this.cardRoles === 'object' ? this.cardRoles : null
1117
+ if (!given || !given.resolve || !this.roles.pill || !this.pillLooks) {
1118
+ return {}
1119
+ }
1120
+ const raw = readPath(card, this.roles.pill)
1121
+ const looks = this.pillLooks[this.roles.pill]
1122
+ return pillLook(looks && raw !== null && raw !== undefined && typeof raw !== 'object' ? looks[String(raw)] : null)
1123
+ },
1124
+
1072
1125
  /**
1073
1126
  * The footer's due text: "Due today", "Due 2 Nov", or "Overdue 2 Nov"
1074
1127
  * for a date in the past. Words, so the late tone is not colour alone.
@@ -107,6 +107,7 @@ Custom card slot — replace the default CnObjectCard with your own rendering:
107
107
  | `selectable` | `Boolean` | `false` | When `true`, each card shows a selection checkbox and the grid emits `select` events |
108
108
  | `selectedIds` | `Array` | `[]` | Array of currently selected object IDs (matched against `object[rowKey]`) |
109
109
  | `accentOf` | `Function` | `null` | `(object) => { variant, icon?, label? } \| null`, each card's status accent (see CnObjectCard `accent`) |
110
+ | `pillLooks` | `Object` | `{}` | The referenced records' colour and icon names, by field and id (`{ [field]: { [id]: { color, icon } } }`), passed to each CnObjectCard for the record layout's pills that declare `resolve`. |
110
111
 
111
112
  ## Board look props
112
113
 
@@ -38,6 +38,7 @@
38
38
  :selected="isSelected(object)"
39
39
  :cardFields="cardFields"
40
40
  :layout="cardLayout"
41
+ :pillLooks="pillLooks"
41
42
  :cardFieldColumns="cardFieldColumns"
42
43
  :status="statusOf ? statusOf(object) : null"
43
44
  :leading="leadingOf ? leadingOf(object) : null"
@@ -179,6 +180,16 @@ export default {
179
180
  default: null,
180
181
  },
181
182
 
183
+ /**
184
+ * The referenced records' colour and icon names, by field and id
185
+ * (`{ [field]: { [id]: { color, icon } } }`), passed through to
186
+ * CnObjectCard for the record layout's pills that declare `resolve`.
187
+ */
188
+ pillLooks: {
189
+ type: Object,
190
+ default: () => ({}),
191
+ },
192
+
182
193
  /** The page's columns by key, passed through to CnObjectCard. */
183
194
  cardFieldColumns: {
184
195
  type: Object,
@@ -50,7 +50,11 @@
50
50
  :label="labelledValue"
51
51
  :variant="badgeVariant"
52
52
  :colorMap="badgeColorMap"
53
- :colorKey="badgeColorKey" />
53
+ :colorKey="badgeColorKey">
54
+ <template v-if="badgeLook.icon" #icon>
55
+ <CnIcon :name="badgeLook.icon" :size="14" class="cn-cell-renderer__badge-icon" />
56
+ </template>
57
+ </CnStatusBadge>
54
58
  <span v-else class="cn-cell-renderer__dash">—</span>
55
59
  </template>
56
60
 
@@ -376,6 +380,7 @@ import CnGroupNameCell from './CnGroupNameCell.vue'
376
380
  import { normalizeLook } from '../../composables/useLook.js'
377
381
  import { ageInDays, boardDateLocale, formatAge, formatBoardDate, formatRelativeDate, formatTimeOfDay, resolveAgeVariant } from '../../utils/boardDate.js'
378
382
  import { parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'
383
+ import { pillLook } from '../../utils/detailActionModel.js'
379
384
  import { resolveDue } from '../../utils/dueText.js'
380
385
  import { formatMoney } from '../../utils/formatMetric.js'
381
386
  import { objectFieldValue } from '../../utils/objectName.js'
@@ -1177,9 +1182,25 @@ export default {
1177
1182
  return Object.keys(map).some((key) => key.toLowerCase() === raw) ? String(this.value) : ''
1178
1183
  },
1179
1184
 
1180
- /** Variant for the built-in `badge` widget — `widgetProps.variant` or `'default'`. */
1185
+ /**
1186
+ * What the built-in `badge` widget takes from the record its value
1187
+ * points at (`widgetProps.looks`, filled by CnIndexPage from
1188
+ * `widgetProps.resolve`): a tone and an icon, or nothing.
1189
+ *
1190
+ * @return {{variant?: string, colorMap?: null, icon?: string}}
1191
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-badge-cell-takes-its-tone-and-icon-from-the-referenced-record
1192
+ */
1193
+ badgeLook() {
1194
+ const looks = this.widgetProps && this.widgetProps.looks
1195
+ if (!looks || typeof looks !== 'object' || !this.hasValue || typeof this.value === 'object') {
1196
+ return {}
1197
+ }
1198
+ return pillLook(looks[String(this.value)])
1199
+ },
1200
+
1201
+ /** Variant for the built-in `badge` widget: the referenced record's tone, else `widgetProps.variant`, else `'default'`. */
1181
1202
  badgeVariant() {
1182
- return (this.widgetProps && this.widgetProps.variant) || 'default'
1203
+ return this.badgeLook.variant || (this.widgetProps && this.widgetProps.variant) || 'default'
1183
1204
  },
1184
1205
 
1185
1206
  /**
@@ -1194,6 +1215,9 @@ export default {
1194
1215
  * @return {object|null}
1195
1216
  */
1196
1217
  badgeColorMap() {
1218
+ if (this.badgeLook.variant) {
1219
+ return null
1220
+ }
1197
1221
  return (this.widgetProps && this.widgetProps.colorMap) || this.enumColorMap || null
1198
1222
  },
1199
1223
 
@@ -2830,6 +2830,7 @@ export default {
2830
2830
  }
2831
2831
 
2832
2832
  .cn-chart-widget__matrix-head {
2833
+ --nldesign-color-on-surface: var(--color-text-maxcontrast);
2833
2834
  color: var(--color-text-maxcontrast);
2834
2835
  padding: 6px;
2835
2836
  }
@@ -2866,7 +2867,11 @@ export default {
2866
2867
  outline-offset: -3px;
2867
2868
  }
2868
2869
 
2870
+ /* A cell is a span: its ink also goes to --nldesign-color-on-surface, which a
2871
+ theme that forces the body ink onto every span (thematiq) reads. Level 3
2872
+ leaves the theme's body ink in place: white on its tint is below 4.5:1. */
2869
2873
  .cn-chart-widget__matrix-cell--1 {
2874
+ --nldesign-color-on-surface: var(--color-primary-element-light-text);
2870
2875
  background: var(--color-primary-element-light);
2871
2876
  color: var(--color-primary-element-light-text);
2872
2877
  }
@@ -2882,6 +2887,7 @@ export default {
2882
2887
  }
2883
2888
 
2884
2889
  .cn-chart-widget__matrix-cell--4 {
2890
+ --nldesign-color-on-surface: var(--color-primary-element-text);
2885
2891
  background: var(--color-primary-element);
2886
2892
  color: var(--color-primary-element-text);
2887
2893
  }
@@ -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"
@@ -1257,6 +1265,7 @@ import {
1257
1265
  MAX_QUICK_ACTIONS,
1258
1266
  normalisePinnedAction,
1259
1267
  pillCountSource,
1268
+ pillLook,
1260
1269
  resolveCountPill,
1261
1270
  resolveNextStep,
1262
1271
  resolvePill,
@@ -2497,9 +2506,12 @@ export default {
2497
2506
  * `field`: `text`, a constant ("Inwoner"), or `countFrom`
2498
2507
  * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`
2499
2508
  * with `{count}`), a count of a related list ("1 lopende zaak") that
2500
- * shows nothing at 0.
2509
+ * shows nothing at 0. With `resolve: { register, schema, labelField?,
2510
+ * colorField?, iconField? }` the pill shows the referenced record's
2511
+ * label, takes its tone from that record's colour (an NL Design System
2512
+ * hue such as `red`, or a badge role) and shows its icon.
2501
2513
  *
2502
- * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string}|null}
2514
+ * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object}|null}
2503
2515
  */
2504
2516
  typePill: {
2505
2517
  type: Object,
@@ -2940,6 +2952,8 @@ export default {
2940
2952
  pillCountSeq: 0,
2941
2953
  /** The labels of the records pills with `resolve` point at, by pill key. */
2942
2954
  pillRefLabels: {},
2955
+ /** The colour and icon names read off the records pills with `resolve` point at, by pill key. */
2956
+ pillRefLooks: {},
2943
2957
  /** Sequence of the last pill label lookup, so a late answer for another record is dropped. */
2944
2958
  pillRefSeq: 0,
2945
2959
  /** `register/schema/id` the read-state call was last made for, so it is sent once per load. */
@@ -3488,10 +3502,11 @@ export default {
3488
3502
  // `resolve`: the referenced record's label replaces the id, and
3489
3503
  // keys the colour too ("Woo-verzoek", not a uuid).
3490
3504
  const resolved = this.pillRefLabels[key]
3505
+ const look = pillLook(this.pillRefLooks[key])
3491
3506
  if (typeof resolved === 'string' && resolved !== '') {
3492
- return { ...pill, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
3507
+ return { ...pill, ...look, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
3493
3508
  }
3494
- return { ...pill, key, label: this.effectiveTranslate(pill.label) }
3509
+ return { ...pill, ...look, key, label: this.effectiveTranslate(pill.label) }
3495
3510
  })
3496
3511
  .filter(Boolean)
3497
3512
  },
@@ -3526,7 +3541,8 @@ export default {
3526
3541
  .filter(([, config]) => config && config.resolve && config.resolve.register && config.resolve.schema && typeof config.field === 'string')
3527
3542
  .map(([key, config]) => {
3528
3543
  const pill = resolvePill({ field: config.field }, this.resolvedObject)
3529
- return pill ? [key, config.resolve.register, config.resolve.schema, config.resolve.labelField || '', pill.colorKey] : null
3544
+ const { register, schema, labelField, colorField, iconField } = config.resolve
3545
+ return pill ? [key, register, schema, labelField || '', pill.colorKey, colorField || '', iconField || ''] : null
3530
3546
  })
3531
3547
  .filter(Boolean)
3532
3548
  },
@@ -5340,6 +5356,7 @@ export default {
5340
5356
  const seq = ++this.pillRefSeq
5341
5357
  if (targets.length === 0) {
5342
5358
  this.pillRefLabels = {}
5359
+ this.pillRefLooks = {}
5343
5360
  return
5344
5361
  }
5345
5362
  if (!this._pillRefResolver) {
@@ -5352,14 +5369,30 @@ export default {
5352
5369
  })
5353
5370
  }
5354
5371
  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(() => ({}))
5372
+ const looks = {}
5373
+ const field = async (register, schema, id, name) => {
5374
+ if (!name) {
5375
+ return null
5376
+ }
5377
+ const found = await this._pillRefResolver.resolve(register, schema, [id], name, { strict: true }).catch(() => ({}))
5378
+ return found && typeof found[id] === 'string' ? found[id] : null
5379
+ }
5380
+ await Promise.all(targets.map(async ([key, register, schema, labelField, id, colorField, iconField]) => {
5381
+ const [found, color, icon] = await Promise.all([
5382
+ this._pillRefResolver.resolve(register, schema, [id], labelField).catch(() => ({})),
5383
+ field(register, schema, id, colorField),
5384
+ field(register, schema, id, iconField),
5385
+ ])
5357
5386
  if (found && typeof found[id] === 'string') {
5358
5387
  labels[key] = found[id]
5359
5388
  }
5389
+ if (color || icon) {
5390
+ looks[key] = { color, icon }
5391
+ }
5360
5392
  }))
5361
5393
  if (seq === this.pillRefSeq) {
5362
5394
  this.pillRefLabels = labels
5395
+ this.pillRefLooks = looks
5363
5396
  }
5364
5397
  },
5365
5398
 
@@ -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;