@conduction/nextcloud-vue 2.56.0-beta.31 → 2.56.0-beta.32

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 (178) hide show
  1. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +55 -4
  2. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  3. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +51 -15
  4. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -2
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
  10. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
  11. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
  12. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
  13. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
  14. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
  15. package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
  16. package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
  17. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
  18. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
  19. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
  20. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
  21. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
  22. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
  23. package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
  24. package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -2
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -0
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +20 -3
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +7 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +77 -3
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +0 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
  51. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
  52. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
  53. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
  54. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
  55. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
  56. package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
  57. package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
  63. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
  64. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
  65. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
  66. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
  67. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
  68. package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
  69. package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
  70. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  71. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
  73. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
  74. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
  75. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
  76. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
  77. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
  78. package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
  79. package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
  84. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
  85. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
  86. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
  87. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
  88. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
  89. package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
  90. package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
  91. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
  92. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  93. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
  94. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  95. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
  96. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  97. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
  98. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
  100. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
  101. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
  102. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
  103. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
  104. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
  105. package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
  106. package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  110. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +4 -2
  111. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  112. package/dist/esm/index.js +7 -0
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/l10n/en.json.js +1 -0
  115. package/dist/esm/l10n/en.json.js.map +1 -1
  116. package/dist/esm/l10n/nl.json.js +1 -0
  117. package/dist/esm/l10n/nl.json.js.map +1 -1
  118. package/dist/esm/nextcloud-vue.css +843 -4
  119. package/dist/esm/utils/contentHeaderLink.js +51 -0
  120. package/dist/esm/utils/contentHeaderLink.js.map +1 -0
  121. package/dist/esm/utils/fetchFilterCounts.js +40 -5
  122. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  123. package/dist/esm/utils/initials.js +32 -0
  124. package/dist/esm/utils/initials.js.map +1 -0
  125. package/dist/esm/utils/libraryWidgetKeys.js +7 -0
  126. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  127. package/dist/esm/utils/objectWrite.js +59 -0
  128. package/dist/esm/utils/objectWrite.js.map +1 -0
  129. package/dist/esm/utils/widgetDispatch.js +1 -1
  130. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  131. package/dist/nextcloud-vue.cjs.js +10510 -6830
  132. package/dist/nextcloud-vue.cjs.js.map +2 -2
  133. package/dist/nextcloud-vue.css +843 -4
  134. package/l10n/en.json +1 -0
  135. package/l10n/nl.json +1 -0
  136. package/package.json +1 -1
  137. package/src/components/CnBannerWidget/CnBannerWidget.vue +93 -13
  138. package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
  139. package/src/components/CnCellRenderer/CnCellRenderer.vue +43 -3
  140. package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
  141. package/src/components/CnChipsFieldWidget/index.js +25 -0
  142. package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
  143. package/src/components/CnChoicePickerWidget/index.js +26 -0
  144. package/src/components/CnDashboardPage/CnDashboardPage.vue +38 -2
  145. package/src/components/CnDataTable/CnDataTable.vue +2 -1
  146. package/src/components/CnDetailPage/CnDetailPage.vue +45 -2
  147. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -3
  148. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +265 -4
  149. package/src/components/CnIndexPage/CnIndexPage.vue +0 -1
  150. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
  151. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
  152. package/src/components/CnRadioCardsWidget/index.js +27 -0
  153. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +43 -1
  154. package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
  155. package/src/components/CnSavedViewsWidget/index.js +29 -0
  156. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
  157. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
  158. package/src/components/CnSettingsFormWidget/index.js +26 -0
  159. package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
  160. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
  161. package/src/components/CnSwitchRowsWidget/index.js +25 -0
  162. package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
  163. package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
  164. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
  165. package/src/components/CnToggleTableWidget/index.js +26 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  167. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +28 -1
  168. package/src/components/index.js +7 -0
  169. package/src/css/look-board-detail.css +36 -2
  170. package/src/css/look-board-index.css +23 -0
  171. package/src/css/look-board.css +72 -0
  172. package/src/index.js +7 -0
  173. package/src/utils/contentHeaderLink.js +48 -0
  174. package/src/utils/fetchFilterCounts.js +39 -4
  175. package/src/utils/initials.js +29 -0
  176. package/src/utils/libraryWidgetKeys.js +7 -0
  177. package/src/utils/objectWrite.js +55 -0
  178. package/src/utils/widgetDispatch.js +1 -1
@@ -0,0 +1,313 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <fieldset class="cn-radio-cards" data-testid="cn-radio-cards" :aria-busy="busy ? 'true' : 'false'">
8
+ <legend v-if="label" class="cn-radio-cards__legend">
9
+ {{ label }}
10
+ </legend>
11
+ <div class="cn-radio-cards__grid" :class="`cn-radio-cards__grid--${columns}`">
12
+ <label
13
+ v-for="option in options"
14
+ :key="String(option.value)"
15
+ class="cn-radio-cards__card"
16
+ :class="{ 'cn-radio-cards__card--selected': isSelected(option) }"
17
+ :data-testid="`cn-radio-cards-option-${option.value}`">
18
+ <input
19
+ class="cn-radio-cards__input"
20
+ type="radio"
21
+ :name="groupName"
22
+ :value="String(option.value)"
23
+ :checked="isSelected(option)"
24
+ :disabled="readOnly || busy"
25
+ @change="choose(option)">
26
+ <span class="cn-radio-cards__dot" aria-hidden="true" />
27
+ <span class="cn-radio-cards__text">
28
+ <span class="cn-radio-cards__title">{{ option.label }}</span>
29
+ <span v-if="option.description" class="cn-radio-cards__description">{{ option.description }}</span>
30
+ </span>
31
+ </label>
32
+ </div>
33
+ <NcNoteCard v-if="notice" type="error" data-testid="cn-radio-cards-notice">
34
+ {{ notice }}
35
+ </NcNoteCard>
36
+ </fieldset>
37
+ </template>
38
+
39
+ <script>
40
+ import { NcNoteCard } from '@nextcloud/vue'
41
+ import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
42
+ import { readPath } from '../../utils/readPath.js'
43
+ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
44
+
45
+ let uid = 0
46
+
47
+ /**
48
+ * CnRadioCardsWidget — the `radio-cards` widget: one choice out of a few cards,
49
+ * each with a title and a description, bound to an enum field of the CURRENT
50
+ * object. Choosing a card saves at once with ONE PATCH of that field; a refused
51
+ * PATCH puts the choice back and shows the server's words. Real radio inputs
52
+ * underneath, so the choice is perceivable without colour and works from the
53
+ * keyboard.
54
+ *
55
+ * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md
56
+ */
57
+ export default {
58
+ name: 'CnRadioCardsWidget',
59
+
60
+ components: { NcNoteCard },
61
+
62
+ inject: {
63
+ cnTranslate: { default: () => (key) => key },
64
+ },
65
+
66
+ props: {
67
+ /**
68
+ * The widget configuration.
69
+ *
70
+ * - `field` (required) — the top-level enum field.
71
+ * - `options` (required) — `[{ value, label, description? }]`.
72
+ * - `label` — a legend over the cards (visually shown).
73
+ * - `columns` — 1, 2 or 3 (default 2).
74
+ * - `readOnly`.
75
+ *
76
+ * @type {object}
77
+ */
78
+ content: {
79
+ type: Object,
80
+ default: () => ({}),
81
+ },
82
+
83
+ /** The current detail-page object. */
84
+ objectData: {
85
+ type: Object,
86
+ default: null,
87
+ },
88
+
89
+ /** The current object id. */
90
+ objectId: {
91
+ type: [String, Number],
92
+ default: '',
93
+ },
94
+
95
+ /** OpenRegister register slug of the surface. */
96
+ register: {
97
+ type: [String, Object],
98
+ default: '',
99
+ },
100
+
101
+ /** OpenRegister schema slug of the surface. */
102
+ schema: {
103
+ type: [String, Object],
104
+ default: '',
105
+ },
106
+ },
107
+
108
+ emits: ['saved'],
109
+
110
+ data() {
111
+ uid += 1
112
+ return {
113
+ groupName: `cn-radio-cards-${uid}`,
114
+ saved: '',
115
+ value: '',
116
+ busy: false,
117
+ notice: '',
118
+ }
119
+ },
120
+
121
+ computed: {
122
+ options() {
123
+ const list = Array.isArray(this.content.options) ? this.content.options : []
124
+ return list
125
+ .filter((option) => option !== null && option !== undefined)
126
+ .map((option) => (typeof option === 'object'
127
+ ? { value: option.value, label: this.tr(String(option.label ?? option.value)), description: this.tr(option.description || '') }
128
+ : { value: option, label: this.tr(String(option)), description: '' }))
129
+ },
130
+
131
+ label() {
132
+ return this.tr(this.content.label || '')
133
+ },
134
+
135
+ columns() {
136
+ return [1, 3].includes(this.content.columns) ? this.content.columns : 2
137
+ },
138
+
139
+ readOnly() {
140
+ return this.content.readOnly === true
141
+ },
142
+
143
+ objectUrl() {
144
+ return objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))
145
+ },
146
+ },
147
+
148
+ watch: {
149
+ objectData() {
150
+ this.adopt()
151
+ },
152
+ },
153
+
154
+ created() {
155
+ this.adopt()
156
+ },
157
+
158
+ methods: {
159
+ tr(text) {
160
+ if (typeof text !== 'string' || text === '') {
161
+ return ''
162
+ }
163
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
164
+ },
165
+
166
+ /** Read the choice off the object. */
167
+ adopt() {
168
+ const stored = readPath(this.objectData || {}, this.content.field)
169
+ this.saved = stored === undefined || stored === null ? '' : String(stored)
170
+ this.value = this.saved
171
+ },
172
+
173
+ isSelected(option) {
174
+ return this.value === String(option.value)
175
+ },
176
+
177
+ /**
178
+ * Choose a card and save that one field.
179
+ *
180
+ * @param {object} option The chosen option.
181
+ * @return {Promise<void>}
182
+ * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
183
+ */
184
+ async choose(option) {
185
+ if (!this.objectUrl || !this.content.field) {
186
+ this.notice = this.tr('This choice cannot be saved here.')
187
+ return
188
+ }
189
+ this.notice = ''
190
+ this.busy = true
191
+ this.value = String(option.value)
192
+ try {
193
+ const data = await patchObject(this.objectUrl, { [this.content.field]: option.value })
194
+ const written = readPath(data, this.content.field)
195
+ this.saved = String(written !== undefined && written !== null ? written : option.value)
196
+ this.value = this.saved
197
+ /**
198
+ * @event saved The choice was written.
199
+ * @type {{field: string, value: unknown}}
200
+ */
201
+ this.$emit('saved', { field: this.content.field, value: option.value })
202
+ } catch (e) {
203
+ this.value = this.saved
204
+ this.notice = serverErrorMessage(e)
205
+ } finally {
206
+ this.busy = false
207
+ }
208
+ },
209
+ },
210
+ }
211
+ </script>
212
+
213
+ <style scoped>
214
+ .cn-radio-cards {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 8px;
218
+ min-width: 0;
219
+ margin: 0;
220
+ padding: 0;
221
+ border: 0;
222
+ }
223
+
224
+ .cn-radio-cards__legend {
225
+ padding: 0;
226
+ margin-bottom: 6px;
227
+ font-weight: 600;
228
+ }
229
+
230
+ .cn-radio-cards__grid {
231
+ display: grid;
232
+ gap: 10px;
233
+ grid-template-columns: minmax(0, 1fr);
234
+ }
235
+
236
+ .cn-radio-cards__grid--2 {
237
+ grid-template-columns: repeat(2, minmax(0, 1fr));
238
+ }
239
+
240
+ .cn-radio-cards__grid--3 {
241
+ grid-template-columns: repeat(3, minmax(0, 1fr));
242
+ }
243
+
244
+ .cn-radio-cards__card {
245
+ position: relative;
246
+ display: flex;
247
+ align-items: flex-start;
248
+ gap: 10px;
249
+ padding: 12px 14px;
250
+ border: 1px solid var(--color-border-maxcontrast);
251
+ border-radius: var(--cn-board-control-radius, 8px);
252
+ background: var(--color-main-background);
253
+ cursor: pointer;
254
+ }
255
+
256
+ .cn-radio-cards__card--selected {
257
+ border-color: var(--color-primary-element);
258
+ background: var(--color-primary-element-light);
259
+ box-shadow: 0 0 0 1px var(--color-primary-element);
260
+ }
261
+
262
+ .cn-radio-cards__card:focus-within {
263
+ outline: 2px solid var(--color-primary-element);
264
+ outline-offset: 2px;
265
+ }
266
+
267
+ /* The real input stays in the DOM and in the accessibility tree; the dot is its picture. */
268
+ .cn-radio-cards__input {
269
+ position: absolute;
270
+ opacity: 0;
271
+ inset: 0;
272
+ margin: 0;
273
+ cursor: pointer;
274
+ }
275
+
276
+ .cn-radio-cards__dot {
277
+ flex: 0 0 auto;
278
+ width: 14px;
279
+ height: 14px;
280
+ margin-top: 3px;
281
+ box-sizing: border-box;
282
+ border: 1px solid var(--color-border-maxcontrast);
283
+ border-radius: 50%;
284
+ background: var(--color-main-background);
285
+ }
286
+
287
+ .cn-radio-cards__card--selected .cn-radio-cards__dot {
288
+ border: 4px solid var(--color-primary-element);
289
+ }
290
+
291
+ .cn-radio-cards__text {
292
+ display: flex;
293
+ flex-direction: column;
294
+ gap: 2px;
295
+ min-width: 0;
296
+ }
297
+
298
+ .cn-radio-cards__title {
299
+ font-weight: 600;
300
+ }
301
+
302
+ .cn-radio-cards__description {
303
+ font-size: 12px;
304
+ color: var(--color-text-maxcontrast);
305
+ }
306
+
307
+ @media (max-width: 640px) {
308
+ .cn-radio-cards__grid--2,
309
+ .cn-radio-cards__grid--3 {
310
+ grid-template-columns: minmax(0, 1fr);
311
+ }
312
+ }
313
+ </style>
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnRadioCardsWidget — self-registers the `radio-cards` widget type (one
6
+ * choice out of a few cards with a title and a description, bound to an enum
7
+ * field of the current object and saved on selection) into the shared
8
+ * dashboardWidgetRegistry at module load.
9
+ */
10
+
11
+ import CnRadioCardsWidget from './CnRadioCardsWidget.vue'
12
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
13
+
14
+ registerDashboardWidget('radio-cards', {
15
+ renderer: CnRadioCardsWidget,
16
+ form: null,
17
+ defaultContent: {
18
+ field: '',
19
+ options: [],
20
+ },
21
+ displayName: 'Radio cards',
22
+ icon: 'RadioboxMarked',
23
+ surfaces: ['detail-page'],
24
+ })
25
+
26
+ export { CnRadioCardsWidget }
27
+ export default CnRadioCardsWidget
@@ -30,6 +30,10 @@
30
30
  class="cn-record-list__row"
31
31
  :class="{ 'cn-record-list__row--link': item.tag !== 'div' }"
32
32
  v-bind="item.attrs">
33
+ <span v-if="item.initials"
34
+ class="cn-record-list__avatar"
35
+ aria-hidden="true"
36
+ data-testid="cn-record-list-avatar">{{ item.initials }}</span>
33
37
  <span class="cn-record-list__main">
34
38
  <span v-if="item.title" class="cn-record-list__title">{{ item.title }}</span>
35
39
  <span v-if="item.meta" class="cn-record-list__meta">{{ item.meta }}</span>
@@ -81,6 +85,7 @@ import CnIcon from '../CnIcon/CnIcon.vue'
81
85
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
82
86
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
83
87
  import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
88
+ import { initialsOf } from '../../utils/initials.js'
84
89
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
85
90
  import { readPath } from '../../utils/readPath.js'
86
91
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
@@ -109,7 +114,8 @@ function unwrap(value) {
109
114
  * objects filtered on a reference to this record. The source keys are those of
110
115
  * `object-list` (`register`, `schema`, `filter` with the `@objectId` /
111
116
  * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
112
- * `rowRoute`, `pageControls`); what differs is the drawing: `presentation` is `rows`, `feed`
117
+ * `rowRoute`, `pageControls`); `avatar` puts a circle with the initials of a name
118
+ * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
113
119
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
114
120
  * `"Termijn {deadline|date}"`).
115
121
  *
@@ -224,6 +230,7 @@ export default {
224
230
  key: (row && (row.id || row['@self']?.id)) || `row-${index}`,
225
231
  tag: link ? 'router-link' : 'div',
226
232
  attrs: link ? { to: link } : {},
233
+ initials: this.initialsFor(row),
227
234
  title: this.fill(c.title, row),
228
235
  meta: this.fill(c.meta, row),
229
236
  text: this.fill(c.text, row),
@@ -305,6 +312,20 @@ export default {
305
312
  return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
306
313
  },
307
314
 
315
+ /**
316
+ * The initials of the row's avatar (`avatar`: a name template such as
317
+ * `"{client.name}"`, or `{ text }`), or '' without one.
318
+ *
319
+ * @param {object} row The row.
320
+ * @return {string} One or two letters.
321
+ * @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar
322
+ */
323
+ initialsFor(row) {
324
+ const avatar = this.content?.avatar
325
+ const template = typeof avatar === 'string' ? avatar : (avatar && typeof avatar === 'object' ? avatar.text : '')
326
+ return initialsOf(this.fill(template, row))
327
+ },
328
+
308
329
  /**
309
330
  * Fill a template from a row, after translating it.
310
331
  *
@@ -504,6 +525,20 @@ export default {
504
525
  text-decoration: underline;
505
526
  }
506
527
 
528
+ .cn-record-list__avatar {
529
+ align-items: center;
530
+ background: var(--color-background-hover);
531
+ border-radius: 50%;
532
+ color: var(--color-main-text);
533
+ display: flex;
534
+ flex: none;
535
+ font-size: 12px;
536
+ font-weight: 700;
537
+ height: 36px;
538
+ justify-content: center;
539
+ width: 36px;
540
+ }
541
+
507
542
  .cn-record-list__main {
508
543
  display: flex;
509
544
  flex: 1 1 auto;
@@ -607,4 +642,11 @@ a.cn-record-list__inline-title {
607
642
  .cn-detail-page__side .cn-record-list__chevron {
608
643
  display: none;
609
644
  }
645
+
646
+ /* A row is a touch target of at least 44px on a phone. */
647
+ @media (max-width: 640px) {
648
+ .cn-record-list__row {
649
+ min-height: 44px;
650
+ }
651
+ }
610
652
  </style>