@conduction/nextcloud-vue 2.110.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 (142) 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/CnAppRoot/CnAppRoot.vue2.js +2 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  23. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  29. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -8
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +98 -63
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +203 -14
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +19 -4
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -1
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  53. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  54. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  61. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  62. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  63. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  64. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  65. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  66. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  67. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  68. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  69. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  70. package/dist/esm/composables/useRefLabels.js +26 -6
  71. package/dist/esm/composables/useRefLabels.js.map +1 -1
  72. package/dist/esm/integrations/registry.js +4 -0
  73. package/dist/esm/integrations/registry.js.map +1 -1
  74. package/dist/esm/l10n/en.json.js +3 -1
  75. package/dist/esm/l10n/en.json.js.map +1 -1
  76. package/dist/esm/l10n/nl.json.js +3 -1
  77. package/dist/esm/l10n/nl.json.js.map +1 -1
  78. package/dist/esm/library-style.js +1 -1
  79. package/dist/esm/nextcloud-vue.css +630 -303
  80. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  81. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  82. package/dist/esm/store/plugins/logs.js +2 -1
  83. package/dist/esm/store/plugins/logs.js.map +1 -1
  84. package/dist/esm/utils/detailActionModel.js +28 -1
  85. package/dist/esm/utils/detailActionModel.js.map +1 -1
  86. package/dist/esm/utils/fillTemplate.js +13 -5
  87. package/dist/esm/utils/fillTemplate.js.map +1 -1
  88. package/dist/esm/utils/headerFieldChips.js +21 -2
  89. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  90. package/dist/esm/utils/mapList.js +26 -11
  91. package/dist/esm/utils/mapList.js.map +1 -1
  92. package/dist/esm/utils/savedViewHelpers.js +40 -3
  93. package/dist/esm/utils/savedViewHelpers.js.map +1 -1
  94. package/dist/esm/utils/savedViewMenu.js +12 -5
  95. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  96. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  97. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  98. package/dist/nextcloud-vue.cjs.js +2760 -1995
  99. package/dist/nextcloud-vue.cjs.js.map +2 -2
  100. package/dist/nextcloud-vue.css +630 -303
  101. package/l10n/en.json +3 -1
  102. package/l10n/nl.json +3 -1
  103. package/package.json +1 -1
  104. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  105. package/src/components/CnAppRoot/CnAppRoot.vue +2 -1
  106. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  107. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  108. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  109. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  110. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  111. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  112. package/src/components/CnDetailPage/CnDetailPage.vue +77 -20
  113. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  114. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  115. package/src/components/CnIndexPage/CnIndexPage.md +1 -1
  116. package/src/components/CnIndexPage/CnIndexPage.vue +211 -16
  117. package/src/components/CnIndexSidebar/CnIndexSidebar.md +1 -1
  118. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +2 -2
  119. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  120. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  121. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  122. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  123. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  124. package/src/composables/useRefLabels.js +25 -5
  125. package/src/css/board.css +5 -2
  126. package/src/css/dialog.css +53 -3
  127. package/src/css/index-page.css +26 -1
  128. package/src/css/look-board-detail.css +55 -22
  129. package/src/css/look-board-index.css +82 -12
  130. package/src/css/look-board-reports.css +1 -1
  131. package/src/css/look-board.css +84 -11
  132. package/src/integrations/registry.js +4 -0
  133. package/src/schemas/app-manifest-v2.schema.json +11 -5
  134. package/src/store/plugins/liveUpdates.js +3 -1
  135. package/src/store/plugins/logs.js +2 -1
  136. package/src/utils/detailActionModel.js +27 -0
  137. package/src/utils/fillTemplate.js +13 -5
  138. package/src/utils/headerFieldChips.js +21 -2
  139. package/src/utils/mapList.js +26 -11
  140. package/src/utils/savedViewHelpers.js +39 -2
  141. package/src/utils/savedViewMenu.js +12 -5
  142. 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.110.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 {
@@ -758,6 +758,7 @@ import { installModalStack, uninstallModalStack } from '../../utils/modalStack.j
758
758
  import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
759
759
  import { resolveBrand } from '../../utils/resolveBrand.js'
760
760
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
761
+ import { pageOffersSavedViews } from '../../utils/savedViewHelpers.js'
761
762
  import { normalizeSavedViewMenu, SAVED_VIEW_MENU_KEY, SAVED_VIEW_MENU_PREFIX, syncSavedViewMenu, toggleSavedViewMenuEntry, withSavedViewMenu } from '../../utils/savedViewMenu.js'
762
763
  import { passesContextPredicates } from '../../utils/visibleIfContext.js'
763
764
 
@@ -2400,7 +2401,7 @@ export default {
2400
2401
  */
2401
2402
  offersSavedViews() {
2402
2403
  const pages = this.manifest && Array.isArray(this.manifest.pages) ? this.manifest.pages : []
2403
- return pages.some((page) => page && page.config && page.config.allowSavedViews === true)
2404
+ return pages.some((page) => page && page.type === 'index' && pageOffersSavedViews(page.config))
2404
2405
  },
2405
2406
 
2406
2407
  /**
@@ -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"
@@ -126,20 +130,13 @@
126
130
  </component>
127
131
  <!-- The one Follow control beside the title (manifest `config.follow`):
128
132
  shown when the object carries `@self.watching`. There is no star:
129
- a favourite is a follow with notifications off. -->
133
+ a favourite is a follow with notifications off. The board look
134
+ draws it on a row of its own under the breadcrumb. -->
130
135
  <span
131
- v-if="showFollowToggle"
136
+ v-if="showFollowToggle && !isBoard"
132
137
  class="cn-detail-page__interactions"
133
138
  data-testid="cn-detail-page-interactions">
134
- <CnFollowToggle
135
- :register="register"
136
- :schema="schema"
137
- :objectId="String(objectId)"
138
- :watching="resolvedSelf.watching === true"
139
- :notify="typeof resolvedSelf.watchNotify === 'boolean' ? resolvedSelf.watchNotify : null"
140
- :watcherCount="typeof resolvedSelf.watcherCount === 'number' ? resolvedSelf.watcherCount : null"
141
- :canManage="!!(resolvedSelf.can && resolvedSelf.can.manage === true)"
142
- :notifies="followNotifies" />
139
+ <CnFollowToggle v-bind="followToggleProps" />
143
140
  </span>
144
141
  </div>
145
142
  <!--
@@ -492,7 +489,11 @@
492
489
  :colorKey="pill.colorKey"
493
490
  :colorMap="pill.colorMap"
494
491
  :variant="pill.variant"
495
- :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>
496
497
  </div>
497
498
  <CnBreadcrumbs
498
499
  v-if="breadcrumbCrumbs.length > 0"
@@ -517,6 +518,17 @@
517
518
  :schema="currentSchema"
518
519
  :register="register" />
519
520
  </div>
521
+ <!-- Row 3 of the board look's header: the Follow control under the
522
+ breadcrumb, clear of the title row and its actions (DqZaak
523
+ "Volgen"). -->
524
+ <div
525
+ v-if="isBoard && showFollowToggle"
526
+ class="cn-detail-page__header-row3"
527
+ data-testid="cn-detail-page-header-row3">
528
+ <span class="cn-detail-page__interactions" data-testid="cn-detail-page-interactions">
529
+ <CnFollowToggle v-bind="followToggleProps" />
530
+ </span>
531
+ </div>
520
532
  <!-- A widget inside the header (manifest `config.headerWidget`), on
521
533
  a row of its own under the title and the actions: the stages
522
534
  bars of the board's case card. Drawn without a card of its own,
@@ -1253,6 +1265,7 @@ import {
1253
1265
  MAX_QUICK_ACTIONS,
1254
1266
  normalisePinnedAction,
1255
1267
  pillCountSource,
1268
+ pillLook,
1256
1269
  resolveCountPill,
1257
1270
  resolveNextStep,
1258
1271
  resolvePill,
@@ -2493,9 +2506,12 @@ export default {
2493
2506
  * `field`: `text`, a constant ("Inwoner"), or `countFrom`
2494
2507
  * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`
2495
2508
  * with `{count}`), a count of a related list ("1 lopende zaak") that
2496
- * 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.
2497
2513
  *
2498
- * @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}
2499
2515
  */
2500
2516
  typePill: {
2501
2517
  type: Object,
@@ -2936,6 +2952,8 @@ export default {
2936
2952
  pillCountSeq: 0,
2937
2953
  /** The labels of the records pills with `resolve` point at, by pill key. */
2938
2954
  pillRefLabels: {},
2955
+ /** The colour and icon names read off the records pills with `resolve` point at, by pill key. */
2956
+ pillRefLooks: {},
2939
2957
  /** Sequence of the last pill label lookup, so a late answer for another record is dropped. */
2940
2958
  pillRefSeq: 0,
2941
2959
  /** `register/schema/id` the read-state call was last made for, so it is sent once per load. */
@@ -3484,10 +3502,11 @@ export default {
3484
3502
  // `resolve`: the referenced record's label replaces the id, and
3485
3503
  // keys the colour too ("Woo-verzoek", not a uuid).
3486
3504
  const resolved = this.pillRefLabels[key]
3505
+ const look = pillLook(this.pillRefLooks[key])
3487
3506
  if (typeof resolved === 'string' && resolved !== '') {
3488
- return { ...pill, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
3507
+ return { ...pill, ...look, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
3489
3508
  }
3490
- return { ...pill, key, label: this.effectiveTranslate(pill.label) }
3509
+ return { ...pill, ...look, key, label: this.effectiveTranslate(pill.label) }
3491
3510
  })
3492
3511
  .filter(Boolean)
3493
3512
  },
@@ -3522,7 +3541,8 @@ export default {
3522
3541
  .filter(([, config]) => config && config.resolve && config.resolve.register && config.resolve.schema && typeof config.field === 'string')
3523
3542
  .map(([key, config]) => {
3524
3543
  const pill = resolvePill({ field: config.field }, this.resolvedObject)
3525
- 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
3526
3546
  })
3527
3547
  .filter(Boolean)
3528
3548
  },
@@ -4319,6 +4339,26 @@ export default {
4319
4339
  && this.register !== '' && this.schema !== '' && this.objectId !== ''
4320
4340
  },
4321
4341
 
4342
+ /**
4343
+ * The props of the Follow control, wherever the header draws it.
4344
+ *
4345
+ * @return {object} The CnFollowToggle props.
4346
+ * @spec openspec/changes/board-follow-row/specs/board-follow-row/spec.md#requirement-the-board-follow-control-sits-under-the-breadcrumb
4347
+ */
4348
+ followToggleProps() {
4349
+ const self = this.resolvedSelf
4350
+ return {
4351
+ register: this.register,
4352
+ schema: this.schema,
4353
+ objectId: String(this.objectId),
4354
+ watching: self.watching === true,
4355
+ notify: typeof self.watchNotify === 'boolean' ? self.watchNotify : null,
4356
+ watcherCount: typeof self.watcherCount === 'number' ? self.watcherCount : null,
4357
+ canManage: !!(self.can && self.can.manage === true),
4358
+ notifies: this.followNotifies,
4359
+ }
4360
+ },
4361
+
4322
4362
  /**
4323
4363
  * The `_extend[]` values of the object read: the page's own, plus
4324
4364
  * `@self.can` while the Follow toggle can render (OpenRegister returns
@@ -5316,6 +5356,7 @@ export default {
5316
5356
  const seq = ++this.pillRefSeq
5317
5357
  if (targets.length === 0) {
5318
5358
  this.pillRefLabels = {}
5359
+ this.pillRefLooks = {}
5319
5360
  return
5320
5361
  }
5321
5362
  if (!this._pillRefResolver) {
@@ -5328,14 +5369,30 @@ export default {
5328
5369
  })
5329
5370
  }
5330
5371
  const labels = {}
5331
- await Promise.all(targets.map(async ([key, register, schema, labelField, id]) => {
5332
- 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
+ ])
5333
5386
  if (found && typeof found[id] === 'string') {
5334
5387
  labels[key] = found[id]
5335
5388
  }
5389
+ if (color || icon) {
5390
+ looks[key] = { color, icon }
5391
+ }
5336
5392
  }))
5337
5393
  if (seq === this.pillRefSeq) {
5338
5394
  this.pillRefLabels = labels
5395
+ this.pillRefLooks = looks
5339
5396
  }
5340
5397
  },
5341
5398