@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
@@ -63,7 +63,7 @@
63
63
  <NcActionLink
64
64
  v-else-if="entry.href"
65
65
  :href="entry.href"
66
- target="_blank"
66
+ :target="entry.linkTarget || '_blank'"
67
67
  rel="noopener noreferrer"
68
68
  :data-testid="`cn-index-header-menu-${entry.key}`">
69
69
  <template v-if="entry.icon" #icon>
@@ -103,6 +103,8 @@
103
103
  <NcButton
104
104
  v-else
105
105
  :variant="button.variant"
106
+ :href="button.link ? button.link.href : undefined"
107
+ :target="button.link ? button.link.target : undefined"
106
108
  :data-testid="`cn-index-header-button-${button.key}`"
107
109
  @click="onHeaderButton(button)">
108
110
  <template v-if="button.icon" #icon>
@@ -947,7 +949,11 @@
947
949
  <CnStatusBadge v-for="(pill, index) in item.pills"
948
950
  :key="index"
949
951
  :label="pill.label"
950
- :variant="pill.variant" />
952
+ :variant="pill.variant">
953
+ <template v-if="pill.icon" #icon>
954
+ <CnIcon :name="pill.icon" :size="14" />
955
+ </template>
956
+ </CnStatusBadge>
951
957
  <span v-if="item.due" class="cn-index-page__map-due">{{ item.due }}</span>
952
958
  </span>
953
959
  </div>
@@ -995,6 +1001,7 @@
995
1001
  :swimlaneField="board.swimlaneField || ''"
996
1002
  :dueRule="board.dueRule || null"
997
1003
  :cardRoles="board.card || null"
1004
+ :pillLooks="refLooks"
998
1005
  :colorField="board.colorField || ''"
999
1006
  :sumField="board.sumField || ''"
1000
1007
  :sumFormat="boardSumFormat"
@@ -1047,7 +1054,7 @@
1047
1054
  v-else-if="currentViewMode === 'list'"
1048
1055
  :objects="displayObjects"
1049
1056
  :schema="effectiveSchema"
1050
- :config="listConfig"
1057
+ :config="renderedListConfig"
1051
1058
  :selectable="selectable"
1052
1059
  :selectedIds="internalSelectedIds"
1053
1060
  :rowKey="rowKey"
@@ -1107,6 +1114,7 @@
1107
1114
  :clickToView="rowClickOpens"
1108
1115
  :cardFields="resolvedCardFields"
1109
1116
  :cardLayout="cardLayout"
1117
+ :pillLooks="refLooks"
1110
1118
  :cardFieldColumns="cardFieldColumns"
1111
1119
  :accentOf="cardAccent"
1112
1120
  :selectedIds="internalSelectedIds"
@@ -1341,8 +1349,8 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
1341
1349
  import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'
1342
1350
  import { METADATA_COLUMNS } from '../../constants/metadata.js'
1343
1351
  import { useObjectStore } from '../../store/useObjectStore.js'
1344
- import { followItemActionLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
1345
- import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
1352
+ import { followItemActionLink, headerActionAsLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
1353
+ import { buildOnSuccessRoute, isExternalActionTarget, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
1346
1354
  import {
1347
1355
  buildReferenceStages,
1348
1356
  buildValueStages,
@@ -1763,6 +1771,8 @@ export default {
1763
1771
 
1764
1772
  inject: {
1765
1773
  cnCustomComponents: { default: () => ({}) },
1774
+ /** The app's named formatters, provided by CnAppRoot, for the map list's pills and template tokens. */
1775
+ cnFormatters: { default: () => ({}) },
1766
1776
  /** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
1767
1777
  cnPermissions: { default: () => [] },
1768
1778
  /** The app config an ancestor provides, for `@config.<key>` in quick-filter labels. */
@@ -3907,11 +3917,16 @@ export default {
3907
3917
  * (unless the handler is the `'none'` keyword) emits
3908
3918
  * `@header-action({ action: id, id })`.
3909
3919
  *
3920
+ * An entry with `href` instead renders as a real link and is never
3921
+ * dispatched or emitted: an absolute `http(s)`, `mailto:`, `tel:` or
3922
+ * `//host/...` URL is used as written, an instance path (leading `/`) goes through
3923
+ * `generateUrl()`. `target: '_blank'` opens it in a new tab.
3924
+ *
3910
3925
  * Reserved ids (those used by built-ins on the bar — `refresh`,
3911
3926
  * `import`, `export`, `copy`, `delete`) are dropped from the
3912
3927
  * merged list with a `console.warn`.
3913
3928
  *
3914
- * @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, disabled?: boolean }>}
3929
+ * @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, href?: string, target?: ('_blank'|'_self'), disabled?: boolean }>}
3915
3930
  */
3916
3931
  headerActions: {
3917
3932
  type: Array,
@@ -4121,6 +4136,8 @@ export default {
4121
4136
  boardPrevPageSize: this.viewMode === 'board' ? ((this.pagination && this.pagination.limit) || 20) : null,
4122
4137
  /** Resolved labels of reference columns: `{ [columnKey]: { [id]: label|null } }`. */
4123
4138
  refLabels: {},
4139
+ /** Colour and icon names of the records badge columns point at: `{ [columnKey]: { [id]: { color, icon } } }`. */
4140
+ refLooks: {},
4124
4141
  /** Count per tab-strip index, for the entries that asked for one; null = none. */
4125
4142
  tabCounts: null,
4126
4143
  // The counts of the `filterStrip` pills, by pill key ('all', 'v0', ...).
@@ -4362,7 +4379,7 @@ export default {
4362
4379
  /**
4363
4380
  * The header actions that open a dialog: `type: "open-form"` (the
4364
4381
  * schema-driven create form, as the navigation's primary action opens
4365
- * it) or `type: "open-modal"`, without a `handler` of their own.
4382
+ * it) or `type: "open-modal"`, without a `handler` or `href` of their own.
4366
4383
  * Reserved ids are left out, as in `mergedHeaderActions`.
4367
4384
  *
4368
4385
  * @spec openspec/changes/index-header-open-form/specs/index-header/spec.md#requirement-an-index-header-action-can-open-the-create-form
@@ -4374,9 +4391,18 @@ export default {
4374
4391
  && typeof entry.id === 'string'
4375
4392
  && !reserved.has(entry.id)
4376
4393
  && (entry.handler === undefined || entry.handler === null || entry.handler === '')
4394
+ && !entry.href
4377
4395
  && (entry.type === 'open-form' || entry.type === 'open-modal'))
4378
4396
  },
4379
4397
 
4398
+ /**
4399
+ * The header actions without reserved ids, each through
4400
+ * `resolveHeaderHandler` (a manifest `href` entry becomes a link with
4401
+ * `_dispatchSuppress`, so the page neither runs nor emits it).
4402
+ *
4403
+ * @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
4404
+ * @return {Array<object>}
4405
+ */
4380
4406
  mergedHeaderActions() {
4381
4407
  const reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])
4382
4408
  const merged = []
@@ -6035,6 +6061,12 @@ export default {
6035
6061
  */
6036
6062
  renderedColumns() {
6037
6063
  let cols = this.tableColumns
6064
+ if (this.refLookSpecs.length > 0) {
6065
+ const lookKeys = new Set(this.refLookSpecs.map((spec) => spec.key))
6066
+ cols = cols.map((col) => (col && typeof col === 'object' && lookKeys.has(col.key)
6067
+ ? { ...col, widgetProps: { ...(col.widgetProps || {}), looks: this.refLooks[col.key] || {} } }
6068
+ : col))
6069
+ }
6038
6070
  if (this.refLabelSpecs.length > 0) {
6039
6071
  const byKey = new Map(this.refLabelSpecs.map((s) => [s.key, s]))
6040
6072
  cols = cols.map((col) => {
@@ -6105,6 +6137,108 @@ export default {
6105
6137
  *
6106
6138
  * @return {Object<string, string[]>} Distinct ids per column key.
6107
6139
  */
6140
+ /**
6141
+ * The pills that take their tone and icon from the record they point at
6142
+ * (`resolve: { colorField?, iconField?, register?, schema? }`): badge
6143
+ * columns and the list view's leading and trailing cells
6144
+ * (`widgetProps.resolve`), the board card's pill (`board.card.resolve`)
6145
+ * and the record card's pills (`cardLayout` headPill, footerPill, chips).
6146
+ * Register and schema default to the field's reference in the schema.
6147
+ *
6148
+ * @return {Array<{key: string, register: string, schema: string, colorField: string, iconField: string}>} The specs.
6149
+ * @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
6150
+ */
6151
+ refLookSpecs() {
6152
+ const list = this.listConfig || {}
6153
+ const listCells = [list.leading, ...(Array.isArray(list.trailing) ? list.trailing : [list.trailing])]
6154
+ const badgeSources = [...this.tableColumns, ...listCells]
6155
+ .filter((c) => c && typeof c === 'object' && c.widget === 'badge' && c.widgetProps)
6156
+ .map((c) => ({ key: c.key, resolve: c.widgetProps.resolve }))
6157
+ // The board card's pill (`board.card.pill` with `board.card.resolve`)
6158
+ // and the record card's pills (`cardLayout` headPill, footerPill,
6159
+ // chips, each with its own `resolve`).
6160
+ const card = this.board && this.board.card && typeof this.board.card === 'object' ? this.board.card : {}
6161
+ const layout = this.cardLayout && typeof this.cardLayout === 'object' ? this.cardLayout : {}
6162
+ const layoutPills = [layout.headPill, layout.footerPill, ...(Array.isArray(layout.chips) ? layout.chips : [])]
6163
+ .filter((def) => def && typeof def === 'object')
6164
+ .map((def) => ({ key: def.field, resolve: def.resolve }))
6165
+ // The map list's pills (`mapConfig.list.pills`, each with its own `resolve`).
6166
+ const mapListPills = (this.mapListEnabled && Array.isArray(this.mapConfig.list.pills) ? this.mapConfig.list.pills : [])
6167
+ .filter((def) => def && typeof def === 'object')
6168
+ .map((def) => ({ key: def.path, resolve: def.resolve }))
6169
+ const seen = new Set()
6170
+ const cols = [...badgeSources, { key: card.pill, resolve: card.resolve }, ...layoutPills, ...mapListPills].filter((c) => {
6171
+ const wanted = typeof c.key === 'string' && c.key !== '' && c.resolve && typeof c.resolve === 'object'
6172
+ && (c.resolve.colorField || c.resolve.iconField) && !seen.has(c.key)
6173
+ if (wanted) {
6174
+ seen.add(c.key)
6175
+ }
6176
+ return wanted
6177
+ })
6178
+ if (cols.length === 0) {
6179
+ return []
6180
+ }
6181
+ const fields = this.effectiveSchema ? fieldsFromSchema(this.effectiveSchema, { includeReadOnly: true }) : []
6182
+ const specs = []
6183
+ for (const col of cols) {
6184
+ const resolve = col.resolve
6185
+ const field = fields.find((f) => f.key === col.key)
6186
+ const ref = (field && field.reference) || {}
6187
+ const register = resolve.register || ref.register || (typeof this.register === 'string' ? this.register : '')
6188
+ const schema = resolve.schema || (ref.schema !== undefined && ref.schema !== null ? String(ref.schema) : '')
6189
+ if (!register || !schema) {
6190
+ continue
6191
+ }
6192
+ specs.push({ key: col.key, register, schema, colorField: resolve.colorField || '', iconField: resolve.iconField || '' })
6193
+ }
6194
+ return specs
6195
+ },
6196
+
6197
+ /**
6198
+ * `listConfig` with the referenced records' looks on its badge cells,
6199
+ * as `renderedColumns` does for the table.
6200
+ *
6201
+ * @return {object} The list view's config.
6202
+ * @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
6203
+ */
6204
+ renderedListConfig() {
6205
+ const list = this.listConfig
6206
+ if (!list || this.refLookSpecs.length === 0) {
6207
+ return list
6208
+ }
6209
+ const lookKeys = new Set(this.refLookSpecs.map((spec) => spec.key))
6210
+ const withLooks = (cell) => (cell && typeof cell === 'object' && lookKeys.has(cell.key)
6211
+ ? { ...cell, widgetProps: { ...(cell.widgetProps || {}), looks: this.refLooks[cell.key] || {} } }
6212
+ : cell)
6213
+ return {
6214
+ ...list,
6215
+ ...(list.leading ? { leading: withLooks(list.leading) } : {}),
6216
+ ...(list.trailing ? { trailing: Array.isArray(list.trailing) ? list.trailing.map(withLooks) : withLooks(list.trailing) } : {}),
6217
+ }
6218
+ },
6219
+
6220
+ /**
6221
+ * The ids each `refLookSpecs` column shows on screen.
6222
+ *
6223
+ * @return {{[key: string]: Array<string>}} Sorted ids by column key.
6224
+ */
6225
+ refLookIds() {
6226
+ const out = {}
6227
+ for (const spec of this.refLookSpecs) {
6228
+ const ids = new Set()
6229
+ for (const row of this.displayObjects) {
6230
+ const v = row ? row[spec.key] : undefined
6231
+ ;(Array.isArray(v) ? v : [v]).forEach((id) => {
6232
+ if (id !== null && id !== undefined && id !== '' && typeof id !== 'object') {
6233
+ ids.add(String(id))
6234
+ }
6235
+ })
6236
+ }
6237
+ out[spec.key] = [...ids].sort()
6238
+ }
6239
+ return out
6240
+ },
6241
+
6108
6242
  refLabelIds() {
6109
6243
  const out = {}
6110
6244
  const facets = this.storeFacets || this.resolvedSidebar.facets || {}
@@ -6372,13 +6506,15 @@ export default {
6372
6506
  const translated = {
6373
6507
  ...list,
6374
6508
  title: this.cnTranslate(list.title),
6375
- sub: list.sub ? this.cnTranslate(list.sub) : '',
6509
+ sub: Array.isArray(list.sub) ? list.sub.map((s) => this.cnTranslate(s)) : (list.sub ? this.cnTranslate(list.sub) : ''),
6376
6510
  due: list.due ? this.cnTranslate(list.due) : '',
6377
6511
  }
6378
6512
  return mapListItems(this.displayObjects, translated, {
6379
6513
  rowKey: this.rowKey,
6380
6514
  located: (row) => Boolean(this.resolveRowGeometry(row)),
6381
6515
  translate: (text) => this.cnTranslate(text),
6516
+ formatters: this.cnFormatters,
6517
+ looks: this.refLooks,
6382
6518
  })
6383
6519
  },
6384
6520
 
@@ -7305,11 +7441,13 @@ export default {
7305
7441
  *
7306
7442
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
7307
7443
  * @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
7308
- * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
7444
+ * @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
7445
+ * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string, link: ({href: string, target: (string|undefined)}|null)}>}
7309
7446
  */
7310
7447
  resolvedHeaderButtons() {
7311
7448
  const headerButtons = this.presented('headerButtons')
7312
7449
  const declared = Array.isArray(headerButtons) ? headerButtons : []
7450
+ const builtInActions = new Set(['add', 'export', 'import', 'refresh', 'save-view', 'actions-menu'])
7313
7451
  return declared
7314
7452
  .filter((button) => button && typeof button === 'object' && typeof button.action === 'string' && button.action !== '')
7315
7453
  .map((button, index) => {
@@ -7336,6 +7474,13 @@ export default {
7336
7474
  if (label === '' && button.action === 'save-view') {
7337
7475
  label = t('nextcloud-vue', 'Save view')
7338
7476
  }
7477
+ const run = typeof button.run === 'string' ? button.run : ''
7478
+ // A button that runs a manifest `href` header action is that link;
7479
+ // a `type: "navigate"` or function-handler entry keeps dispatching.
7480
+ const actionId = button.action === 'export' ? run : (builtInActions.has(button.action) ? '' : button.action)
7481
+ const linked = actionId
7482
+ ? this.mergedHeaderActions.find((entry) => entry && entry.id === actionId && entry.href && entry._dispatchSuppress && (!entry.type || entry.type === 'handler'))
7483
+ : null
7339
7484
  return {
7340
7485
  key: String(button.id || button.action || index),
7341
7486
  label,
@@ -7343,7 +7488,8 @@ export default {
7343
7488
  variant: button.variant === 'primary' ? 'primary' : 'secondary',
7344
7489
  icon,
7345
7490
  format: button.format === 'excel' ? 'excel' : 'csv',
7346
- run: typeof button.run === 'string' ? button.run : '',
7491
+ run,
7492
+ link: linked ? { href: linked.href, target: linked.linkTarget || undefined } : null,
7347
7493
  items: Array.isArray(button.items)
7348
7494
  ? button.items.filter((item) => (typeof item === 'string' && item !== '')
7349
7495
  || (item && typeof item === 'object' && typeof item.heading === 'string' && item.heading !== ''))
@@ -8002,6 +8148,15 @@ export default {
8002
8148
  },
8003
8149
  },
8004
8150
 
8151
+ // Read the colour and icon of the records badge columns point at.
8152
+ refLookIds: {
8153
+ immediate: true,
8154
+ deep: true,
8155
+ handler() {
8156
+ this.loadRefLooks()
8157
+ },
8158
+ },
8159
+
8005
8160
  // The Active line names a referenced value, not its id.
8006
8161
  activeFilterRefIds: {
8007
8162
  deep: true,
@@ -8453,6 +8608,45 @@ export default {
8453
8608
  }))
8454
8609
  },
8455
8610
 
8611
+ /**
8612
+ * Read the colour and icon names `refLookSpecs` columns need, for the
8613
+ * rows on screen, with the same resolver as the labels. A failure
8614
+ * leaves the badge as declared.
8615
+ *
8616
+ * @return {Promise<void>}
8617
+ * @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
8618
+ */
8619
+ async loadRefLooks() {
8620
+ if (this.refLookSpecs.length === 0) {
8621
+ return
8622
+ }
8623
+ if (!this._refLabelResolver) {
8624
+ this._refLabelResolver = createRefLabelResolver(() => {
8625
+ try {
8626
+ return (this.list && this.list.objectStore) || useObjectStore()
8627
+ } catch {
8628
+ return null
8629
+ }
8630
+ })
8631
+ }
8632
+ const read = (spec, ids, name) => (name
8633
+ ? this._refLabelResolver.resolve(spec.register, spec.schema, ids, name, { strict: true }).catch(() => ({}))
8634
+ : Promise.resolve({}))
8635
+ await Promise.all(this.refLookSpecs.map(async (spec) => {
8636
+ const known = this.refLooks[spec.key] || {}
8637
+ const ids = (this.refLookIds[spec.key] || []).filter((id) => !Object.hasOwn(known, id))
8638
+ if (ids.length === 0) {
8639
+ return
8640
+ }
8641
+ const [colors, icons] = await Promise.all([read(spec, ids, spec.colorField), read(spec, ids, spec.iconField)])
8642
+ const found = {}
8643
+ ids.forEach((id) => {
8644
+ found[id] = { color: (colors && colors[id]) || null, icon: (icons && icons[id]) || null }
8645
+ })
8646
+ this.refLooks = { ...this.refLooks, [spec.key]: { ...(this.refLooks[spec.key] || {}), ...found } }
8647
+ }))
8648
+ },
8649
+
8456
8650
  /**
8457
8651
  * Whether a saved view shows its record count (`viewCounts`).
8458
8652
  *
@@ -8953,8 +9147,11 @@ export default {
8953
9147
  * manifest-actions-dispatch — `navigate`, `emit`, `none`, or a
8954
9148
  * registry name (resolved through `resolveRegisteredHandler`).
8955
9149
  *
9150
+ * @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
8956
9151
  * @param {object} entry Raw headerActions entry.
8957
- * @return {object} Possibly-mutated copy: function-typed
9152
+ * @return {object} Possibly-mutated copy: an `href` becomes a
9153
+ * resolved link with `linkTarget`, no handler (a function one
9154
+ * included) and `_dispatchSuppress: true`; function-typed
8958
9155
  * handlers untouched; `'emit'` and unknown registry names
8959
9156
  * strip the `handler` (emit-only fall-through); `'navigate'`
8960
9157
  * becomes a `$router.push` thunk (with the entry's literal
@@ -8966,14 +9163,23 @@ export default {
8966
9163
  if (!entry) {
8967
9164
  return entry
8968
9165
  }
9166
+ // An `href` makes the entry a link, which only navigates: it
9167
+ // carries no handler, and the page emits nothing for it.
9168
+ if (typeof entry.href === 'string' && entry.href !== '') {
9169
+ const link = headerActionAsLink(entry)
9170
+ return link.href ? { ...link, linkTarget: link.target, _dispatchSuppress: true } : link
9171
+ }
8969
9172
  const handler = entry.handler
8970
9173
  if (typeof handler === 'function') {
8971
9174
  return { ...entry }
8972
9175
  }
8973
9176
  // `type: "navigate"` with a `target` URL is a link: the bar and the
8974
- // header's Actions menu render it as a real anchor in a new tab.
9177
+ // header's Actions menu render it as a real anchor, an external URL
9178
+ // in a new tab.
8975
9179
  if (typeof handler !== 'string' && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '' && !entry.href) {
8976
- return { ...entry, href: entry.target }
9180
+ return isExternalActionTarget(entry.target)
9181
+ ? { ...entry, href: entry.target, linkTarget: '_blank' }
9182
+ : { ...entry, href: entry.target }
8977
9183
  }
8978
9184
  if (typeof handler !== 'string') {
8979
9185
  // No handler declared — emit-only fall-through.
@@ -9144,8 +9350,9 @@ export default {
9144
9350
  *
9145
9351
  * @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
9146
9352
  * @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
9353
+ * @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
9147
9354
  * @param {{items?: Array<(string|{heading: string})>|null, format?: string}} button The resolved header button.
9148
- * @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, disabled?: boolean, title?: string, iconComponent?: object, run?: Function}>}
9355
+ * @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, linkTarget?: string, disabled?: boolean, title?: string, iconComponent?: object, run?: () => void}>}
9149
9356
  */
9150
9357
  headerMenuEntries(button) {
9151
9358
  const fromAction = (entry) => ({
@@ -9153,6 +9360,7 @@ export default {
9153
9360
  label: entry.label ? this.cnTranslate(entry.label) : '',
9154
9361
  icon: entry.icon && typeof entry.icon === 'string' && !entry.icon.startsWith('icon-') ? entry.icon : '',
9155
9362
  href: typeof entry.href === 'string' && entry.href !== '' ? entry.href : '',
9363
+ linkTarget: entry.linkTarget || '',
9156
9364
  // A `handler: "navigate"` entry whose route resolves carries `to`
9157
9365
  // (resolveHeaderHandler drops its handler): the menu draws it as
9158
9366
  // an in-app link, as the actions bar does, so it navigates.
@@ -9186,7 +9394,7 @@ export default {
9186
9394
  }),
9187
9395
 
9188
9396
  documentation: () => (this.documentationUrl
9189
- ? { key: 'documentation', label: this.documentationLabel || t('nextcloud-vue', 'Documentation'), icon: 'BookOpenVariantOutline', href: this.documentationUrl }
9397
+ ? { key: 'documentation', label: this.documentationLabel || t('nextcloud-vue', 'Documentation'), icon: 'BookOpenVariantOutline', href: this.documentationUrl, linkTarget: '_blank' }
9190
9398
  : null),
9191
9399
  }
9192
9400
  const entries = []
@@ -9282,13 +9490,18 @@ export default {
9282
9490
 
9283
9491
  /**
9284
9492
  * A header button was clicked: run its built-in action, or dispatch
9285
- * it as the `headerActions` entry with that id.
9493
+ * it as the `headerActions` entry with that id. A button that is a
9494
+ * link is left to the browser.
9286
9495
  *
9287
9496
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
9288
- * @param {{action: string, format: string}} button The resolved button.
9497
+ * @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
9498
+ * @param {{action: string, format: string, link?: object}} button The resolved button.
9289
9499
  * @return {void}
9290
9500
  */
9291
9501
  onHeaderButton(button) {
9502
+ if (button.link) {
9503
+ return
9504
+ }
9292
9505
  if (button.action === 'add') {
9293
9506
  this.onAddClick()
9294
9507
  } else if (button.action === 'export') {
@@ -11614,7 +11827,7 @@ export default {
11614
11827
  return
11615
11828
  }
11616
11829
  const badge = this.savedViewMenuBadge(view)
11617
- menu.toggle({ id: String(view.id || view.slug), page: this.savedViewPage, name: view.name, ...(badge ? { badge } : {}) })
11830
+ menu.toggle({ id: String(view.id || view.slug), page: this.savedViewPage, name: view.name, ...(typeof view.icon === 'string' && view.icon !== '' ? { icon: view.icon } : {}), ...(badge ? { badge } : {}) })
11618
11831
  },
11619
11832
 
11620
11833
  /**
@@ -12149,7 +12362,9 @@ export default {
12149
12362
  if (resultData && resultData.success && this._refLabelResolver) {
12150
12363
  this._refLabelResolver.invalidate()
12151
12364
  this.refLabels = {}
12365
+ this.refLooks = {}
12152
12366
  this.loadRefLabels()
12367
+ this.loadRefLooks()
12153
12368
  }
12154
12369
  },
12155
12370
 
@@ -32,6 +32,7 @@
32
32
  import { translate as t } from '@nextcloud/l10n'
33
33
  import { generateUrl } from '@nextcloud/router'
34
34
  import { fetchEndpointSource } from '../../composables/useEndpointSource.js'
35
+ import { resolveDue } from '../../utils/dueText.js'
35
36
  import { fillTemplate } from '../../utils/fillTemplate.js'
36
37
  import { openRowTarget } from '../../utils/linkNavigation.js'
37
38
  import { readPath } from '../../utils/readPath.js'
@@ -70,6 +71,9 @@ function appUrl(url) {
70
71
  * - `itemsPath`: the rows' path in the response, when not `results`.
71
72
  * - `limit`: the most rows shown (default 7).
72
73
  * - `title`, `subtitle`: templates filled from a row (default `{title}`, none).
74
+ * - `due`: `{ field, ...dueText config }`: the trailing line says how a date
75
+ * stands ("3 days overdue", "Due today", the date or the days left), in
76
+ * place of `subtitle` (see `resolveDue`).
73
77
  * - `href`: a template for a row's page (`/apps/dossiq/cases/{id}`).
74
78
  * - `viewAll`: `{ href, label? }`.
75
79
  * - `emptyText`: text for an empty list.
@@ -125,7 +129,7 @@ export default {
125
129
  return {
126
130
  id: String(id),
127
131
  mainText: this.fill(this.config.title || '{title}', item) || String(id),
128
- subText: this.fill(this.config.subtitle, item),
132
+ subText: this.subtitleOf(item),
129
133
  href: this.config.href ? appUrl(fillTemplate(this.config.href, { ...item, id })) : '',
130
134
  }
131
135
  })
@@ -155,6 +159,22 @@ export default {
155
159
  return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
156
160
  },
157
161
 
162
+ /**
163
+ * A row's trailing line: how its due date stands, or the subtitle template.
164
+ *
165
+ * @spec openspec/changes/nc-dashboard-due/specs/nc-dashboard-widgets/spec.md#requirement-a-dashboard-row-says-how-its-date-stands
166
+ * @param {object} item The row.
167
+ * @return {string}
168
+ */
169
+ subtitleOf(item) {
170
+ const due = this.config.due
171
+ if (due && typeof due === 'object' && due.field) {
172
+ const resolved = resolveDue(readPath(item, due.field), due, (text) => this.cnTranslate(text))
173
+ return resolved ? resolved.text : ''
174
+ }
175
+ return this.fill(this.config.subtitle, item)
176
+ },
177
+
158
178
  /**
159
179
  * A template filled from a row, translated first.
160
180
  *
@@ -138,6 +138,7 @@ export default {
138
138
  |---|---|---|---|
139
139
  | `maxMetadata` | `Number` | `4` | Maximum number of schema properties to show in the metadata section. Properties are sorted by their `order` value before slicing |
140
140
  | `accent` | `Object` | `null` | Status accent `{ variant, icon?, label? }`: a colored start border and, with `icon`, a colored icon before the title. `variant` is `success`, `warning`, `error`, `info` or `primary`; `label` is the icon's tooltip and accessible name |
141
+ | `pillLooks` | `Object` | `{}` | The referenced records' colour and icon names, by field and id (`{ [field]: { [id]: { color, icon } } }`, read by CnIndexPage). A record layout pill with `resolve: { colorField?, iconField? }` takes its record's tone and icon from here. |
141
142
 
142
143
  ## Slots
143
144
 
@@ -24,7 +24,11 @@
24
24
  class="cn-object-card__record-pill"
25
25
  :label="recordHeadPill.label"
26
26
  :variant="recordHeadPill.variant"
27
- data-testid="cn-object-card-head-pill" />
27
+ data-testid="cn-object-card-head-pill">
28
+ <template v-if="recordHeadPill.icon" #icon>
29
+ <CnIcon :name="recordHeadPill.icon" :size="14" />
30
+ </template>
31
+ </CnStatusBadge>
28
32
  <span v-if="recordNumber" class="cn-object-card__record-number" data-testid="cn-object-card-number">{{ recordNumber }}</span>
29
33
  <div
30
34
  v-if="$slots.actions"
@@ -63,7 +67,11 @@
63
67
  :key="chip.field"
64
68
  class="cn-object-card__record-pill"
65
69
  :label="chip.label"
66
- :variant="chip.variant" />
70
+ :variant="chip.variant">
71
+ <template v-if="chip.icon" #icon>
72
+ <CnIcon :name="chip.icon" :size="14" />
73
+ </template>
74
+ </CnStatusBadge>
67
75
  </div>
68
76
  <div v-if="$slots.badges" class="cn-object-card__badges">
69
77
  <slot name="badges" :object="object" />
@@ -74,7 +82,11 @@
74
82
  class="cn-object-card__record-pill"
75
83
  :label="recordFootPill.label"
76
84
  :variant="recordFootPill.variant"
77
- data-testid="cn-object-card-foot-pill" />
85
+ data-testid="cn-object-card-foot-pill">
86
+ <template v-if="recordFootPill.icon" #icon>
87
+ <CnIcon :name="recordFootPill.icon" :size="14" />
88
+ </template>
89
+ </CnStatusBadge>
78
90
  <span
79
91
  v-if="recordDate"
80
92
  class="cn-object-card__record-date"
@@ -207,6 +219,7 @@ import { normalizeLook } from '../../composables/useLook.js'
207
219
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
208
220
  import { formatBoardDate } from '../../utils/boardDate.js'
209
221
  import { resolveDateVariant } from '../../utils/dateVariant.js'
222
+ import { pillLook } from '../../utils/detailActionModel.js'
210
223
  import { resolveDue } from '../../utils/dueText.js'
211
224
  import { readPath } from '../../utils/readPath.js'
212
225
  import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
@@ -375,6 +388,20 @@ export default {
375
388
  default: null,
376
389
  },
377
390
 
391
+ /**
392
+ * The referenced records' colour and icon names, by field and id
393
+ * (`{ [field]: { [id]: { color, icon } } }`, CnIndexPage reads them).
394
+ * A record layout pill with `resolve: { colorField?, iconField? }` takes
395
+ * its record's tone and icon from here.
396
+ *
397
+ * @type {object}
398
+ * @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
399
+ */
400
+ pillLooks: {
401
+ type: Object,
402
+ default: () => ({}),
403
+ },
404
+
378
405
  /**
379
406
  * The property keys the facts list shows, in order (manifest key
380
407
  * `config.cardFields`). Without it the card shows the first
@@ -588,7 +615,7 @@ export default {
588
615
  const labels = def.labels && typeof def.labels === 'object' ? def.labels : {}
589
616
  const shown = typeof labels[String(raw)] === 'string' ? this.cnTranslate(labels[String(raw)]) : label
590
617
  const variant = mapped || def.variant
591
- return { field: def.field, label: shown, variant: isBadgeVariant(variant) ? variant : 'default' }
618
+ return this.withLook({ field: def.field, label: shown, variant: isBadgeVariant(variant) ? variant : 'default' }, def)
592
619
  })
593
620
  .filter(Boolean)
594
621
  },
@@ -838,7 +865,26 @@ export default {
838
865
  const colorMap = def.colorMap && typeof def.colorMap === 'object' ? def.colorMap : {}
839
866
  const key = Object.keys(colorMap).find((k) => k.toLowerCase() === value.toLowerCase())
840
867
  const variant = key !== undefined ? colorMap[key] : def.variant
841
- return { label, variant: isBadgeVariant(variant) ? variant : 'default' }
868
+ return this.withLook({ label, variant: isBadgeVariant(variant) ? variant : 'default' }, def)
869
+ },
870
+
871
+ /**
872
+ * Lay the referenced record's tone and icon over a layout pill that
873
+ * declares `resolve` (see `pillLooks`).
874
+ *
875
+ * @param {{label: string, variant: string}} pill The pill as declared.
876
+ * @param {object} def The pill's definition.
877
+ * @return {{label: string, variant: string, icon?: string}} The pill.
878
+ * @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
879
+ */
880
+ withLook(pill, def) {
881
+ if (!def || !def.resolve || !this.pillLooks || typeof this.pillLooks !== 'object') {
882
+ return pill
883
+ }
884
+ const looks = this.pillLooks[def.field]
885
+ const raw = readPath(this.object, def.field)
886
+ const look = pillLook(looks && raw !== null && raw !== undefined ? looks[String(raw)] : null)
887
+ return { ...pill, ...(look.variant ? { variant: look.variant } : {}), ...(look.icon ? { icon: look.icon } : {}) }
842
888
  },
843
889
 
844
890
  /**