@conduction/nextcloud-vue 2.150.0 → 2.151.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 (67) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +40 -41
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  7. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +6 -3
  8. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  9. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +13 -1
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  11. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  12. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +5 -1
  13. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  16. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +46 -0
  17. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  18. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +64 -64
  19. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  20. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  21. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +15 -1
  22. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +1 -1
  25. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -1
  26. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +16 -5
  27. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -1
  28. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  29. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +25 -2
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +82 -68
  32. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  34. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  37. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +1 -1
  39. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  40. package/dist/esm/integrations/builtin/calendar/eventStatus.js +43 -2
  41. package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -1
  42. package/dist/esm/l10n/en.json.js +4 -1
  43. package/dist/esm/l10n/en.json.js.map +1 -1
  44. package/dist/esm/l10n/nl.json.js +5 -2
  45. package/dist/esm/l10n/nl.json.js.map +1 -1
  46. package/dist/esm/library-style.js +1 -1
  47. package/dist/esm/nextcloud-vue.css +623 -362
  48. package/dist/nextcloud-vue.cjs.js +894 -720
  49. package/dist/nextcloud-vue.cjs.js.map +2 -2
  50. package/dist/nextcloud-vue.css +623 -362
  51. package/l10n/en.json +4 -1
  52. package/l10n/nl.json +5 -2
  53. package/package.json +1 -1
  54. package/src/components/CnActionDialog/CnActionDialog.vue +17 -4
  55. package/src/components/CnConversationThread/CnConversationThread.vue +16 -2
  56. package/src/components/CnConversationThread/CnConversationWidget.vue +5 -1
  57. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +47 -2
  58. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  59. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +15 -1
  60. package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +17 -2
  61. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +45 -2
  62. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +8 -1
  63. package/src/components/CnTimelineWidget/CnTimelineManage.vue +1 -1
  64. package/src/css/dialog.css +6 -0
  65. package/src/css/look-board-detail.css +137 -0
  66. package/src/css/look-board.css +78 -2
  67. package/src/integrations/builtin/calendar/eventStatus.js +42 -1
package/l10n/en.json CHANGED
@@ -2399,7 +2399,10 @@
2399
2399
  "More actions for {name}": "More actions for {name}",
2400
2400
  "Unlink contact": "Unlink contact",
2401
2401
  "{term}, extendable by {extension}": "{term}, extendable by {extension}",
2402
- "Move {item}, use the arrow keys": "Move {item}, use the arrow keys"
2402
+ "Move {item}, use the arrow keys": "Move {item}, use the arrow keys",
2403
+ "Invited": "Invited",
2404
+ "Accepted": "Accepted",
2405
+ "Declined": "Declined"
2403
2406
  },
2404
2407
  "plurals": {
2405
2408
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2008,7 +2008,7 @@
2008
2008
  "Mail is not available": "E-mail is niet beschikbaar",
2009
2009
  "Install Nextcloud Mail to draft and link mail here.": "Installeer Nextcloud Mail om hier e-mail op te stellen en te koppelen.",
2010
2010
  "Email template": "E-mailsjabloon",
2011
- "Open draft from template": "Concept openen vanuit sjabloon",
2011
+ "Open draft from template": "Concept openen uit sjabloon",
2012
2012
  "Empty draft": "Leeg concept",
2013
2013
  "Could not open draft": "Kon concept niet openen",
2014
2014
  "Some template fields could not be filled. Fill these in the draft yourself:": "Sommige sjabloonvelden konden niet worden ingevuld. Vul deze zelf in het concept in:",
@@ -2654,7 +2654,10 @@
2654
2654
  "More actions for {name}": "Meer acties voor {name}",
2655
2655
  "Unlink contact": "Contact ontkoppelen",
2656
2656
  "{term}, extendable by {extension}": "{term}, te verdagen met {extension}",
2657
- "Move {item}, use the arrow keys": "{item} verplaatsen, gebruik de pijltjestoetsen"
2657
+ "Move {item}, use the arrow keys": "{item} verplaatsen, gebruik de pijltjestoetsen",
2658
+ "Invited": "Uitgenodigd",
2659
+ "Accepted": "Geaccepteerd",
2660
+ "Declined": "Afgewezen"
2658
2661
  },
2659
2662
  "plurals": {
2660
2663
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.150.0",
3
+ "version": "2.151.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": {
@@ -21,7 +21,10 @@
21
21
  <strong>{{ summary.title }}</strong>
22
22
  <span v-if="summary.meta" class="cn-action-dialog__muted">{{ summary.meta }}</span>
23
23
  </span>
24
- <span v-if="summary.badge" class="cn-action-dialog__badge">{{ summary.badge }}</span>
24
+ <span
25
+ v-if="summary.badge"
26
+ class="cn-action-dialog__badge"
27
+ :class="summary.badgeVariant ? 'cn-status-badge--' + summary.badgeVariant : ''">{{ summary.badge }}</span>
25
28
  </div>
26
29
 
27
30
  <!-- Facts about the record, in titled groups ("Retention under the case type"). -->
@@ -218,7 +221,7 @@ let reasonSeq = 0
218
221
  * `config`:
219
222
  * - `title`, `subtitle`, `doneText`: texts, through the host translate;
220
223
  * `eyebrow` a template filled from the record ("Close off · {identifier}").
221
- * - `summary`: `{ source?, title, meta?, badge?, icon? }`: templates filled from
224
+ * - `summary`: `{ source?, title, meta?, badge?, badgeVariant?, badgeColorMap?, icon? }`: templates filled from
222
225
  * the record (`source` an endpointSource to read it from, else `object`).
223
226
  * - `facts`: `[{ title?, items: [{ label, value }] }]`, `value` a template; a
224
227
  * `{path|<formatter>}` token reads through the built-in and app formatters.
@@ -355,7 +358,12 @@ export default {
355
358
  return null
356
359
  }
357
360
  const title = this.fill(def.title)
358
- return title ? { title, meta: this.fill(def.meta), badge: this.fill(def.badge) } : null
361
+ const badge = this.fill(def.badge)
362
+ const variants = ['default', 'success', 'warning', 'error', 'info', 'purple', 'teal']
363
+ const mapped = def.badgeColorMap && typeof def.badgeColorMap === 'object' ? def.badgeColorMap[badge] : undefined
364
+ const wanted = mapped || def.badgeVariant
365
+ const badgeVariant = variants.includes(wanted) ? wanted : ''
366
+ return title ? { title, meta: this.fill(def.meta), badge, badgeVariant } : null
359
367
  },
360
368
 
361
369
  /** @return {Array<{title: string, facts: Array<{label: string, value: string}>}>} The fact groups. */
@@ -858,12 +866,17 @@ export default {
858
866
  .cn-action-dialog__badge {
859
867
  padding: 3px 10px;
860
868
  border-radius: 12px;
861
- background: var(--color-background-dark);
862
869
  font-size: 13px;
863
870
  font-weight: 600;
871
+ line-height: normal;
864
872
  white-space: nowrap;
865
873
  }
866
874
 
875
+ /* Without a tone (`badgeVariant`, `badgeColorMap`) the badge is neutral. */
876
+ .cn-action-dialog__badge:not([class*='cn-status-badge--']) {
877
+ background: var(--color-background-dark);
878
+ }
879
+
867
880
  .cn-action-dialog__title {
868
881
  margin: 0 0 8px;
869
882
  font-size: 14px;
@@ -64,7 +64,11 @@
64
64
  <span class="cn-conversation-thread__meta">
65
65
  <span class="hidden-visually">{{ row.directionLabel }}</span>
66
66
  <span class="cn-conversation-thread__author">{{ row.author }}</span>
67
- <span v-if="row.channel" class="cn-conversation-thread__channel" data-testid="cn-conversation-channel">{{ row.channel }}</span>
67
+ <span v-if="row.channel" class="cn-conversation-thread__channel" data-testid="cn-conversation-channel"><CnIcon v-if="row.channelIcon"
68
+ class="cn-conversation-thread__channel-icon"
69
+ :name="row.channelIcon"
70
+ :size="13"
71
+ aria-hidden="true" />{{ row.channel }}</span>
68
72
  <span v-if="row.status" class="cn-conversation-thread__status" data-testid="cn-conversation-status">{{ row.status }}</span>
69
73
  <span v-if="showDirection"
70
74
  class="cn-conversation-thread__direction"
@@ -151,6 +155,7 @@ import axios from '@nextcloud/axios'
151
155
  import { translate as t } from '@nextcloud/l10n'
152
156
  import { NcButton } from '@nextcloud/vue'
153
157
  import Send from 'vue-material-design-icons/Send.vue'
158
+ import CnIcon from '../CnIcon/CnIcon.vue'
154
159
  import CnQuickFilterBar from '../CnQuickFilterBar/CnQuickFilterBar.vue'
155
160
  import { prefixUrl } from '../../utils/headers.js'
156
161
  import { userLocale } from '../../utils/userLocale.js'
@@ -185,6 +190,7 @@ export default {
185
190
  name: 'CnConversationThread',
186
191
 
187
192
  components: {
193
+ CnIcon,
188
194
  CnQuickFilterBar,
189
195
  NcButton,
190
196
  Send,
@@ -197,7 +203,7 @@ export default {
197
203
 
198
204
  props: {
199
205
  /**
200
- * The messages, oldest first: `{ id?, author?, time?, text, side?, channel?, status?, actions?, note? }`.
206
+ * The messages, oldest first: `{ id?, author?, time?, text, side?, channel?, channelIcon?, status?, actions?, note? }`.
201
207
  * `status` is shown after the channel ("Failed"); `actions` are
202
208
  * `{ id, label, disabled? }` buttons under the text, each emitting
203
209
  * `message-action`.
@@ -381,6 +387,7 @@ export default {
381
387
  timeLabel: time.label,
382
388
  datetime: time.datetime,
383
389
  channel: typeof message.channel === 'string' ? message.channel : '',
390
+ channelIcon: typeof message.channelIcon === 'string' ? message.channelIcon : '',
384
391
  status: typeof message.status === 'string' ? message.status : '',
385
392
  actions: Array.isArray(message.actions) ? message.actions.filter((action) => action && action.id && action.label) : [],
386
393
  directionLabel: side === 'us' ? t('nextcloud-vue', 'Sent message') : t('nextcloud-vue', 'Received message'),
@@ -787,4 +794,11 @@ export default {
787
794
  .cn-conversation-thread__channel {
788
795
  color: var(--color-text-maxcontrast);
789
796
  }
797
+
798
+ /* The channel's icon (`channelIcon`) stands before its name, 5px apart. */
799
+ .cn-conversation-thread__channel:has(.cn-conversation-thread__channel-icon) {
800
+ display: inline-flex;
801
+ align-items: center;
802
+ gap: 5px;
803
+ }
790
804
  </style>
@@ -109,7 +109,8 @@ import { userDisplayName } from '../../utils/userDisplayName.js'
109
109
  * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias
110
110
  * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias
111
111
  * `outValue`) for the messages the organisation sent, and `channelField` with
112
- * an optional `channelMap` from stored value to label ("email" to "E-mail").
112
+ * an optional `channelMap` from stored value to label ("email" to "E-mail")
113
+ * and `channelIcons` from stored value to an icon name, drawn before the label.
113
114
  *
114
115
  * The reply box posts to `endpoint` (`@objectId` filled in), or runs
115
116
  * `replyAction`, an action descriptor the app decides the meaning of: it is
@@ -294,6 +295,7 @@ export default {
294
295
  const c = this.content || {}
295
296
  const usValue = String(c.outValue ?? c.usValue ?? 'us')
296
297
  const channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}
298
+ const channelIcons = c.channelIcons && typeof c.channelIcons === 'object' ? c.channelIcons : {}
297
299
  const statusMap = c.statusMap && typeof c.statusMap === 'object' ? c.statusMap : {}
298
300
  const stored = this.rawMessages()
299
301
  .map(({ message, side, key }, index) => {
@@ -310,6 +312,8 @@ export default {
310
312
  text: readPath(message, c.bodyField || c.textField || 'text'),
311
313
  side: sideOf,
312
314
  channel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),
315
+ // @spec openspec/changes/conversation-channel-icons/specs/conversation-channel-icons/spec.md#requirement-a-messages-channel-can-carry-its-icon
316
+ channelIcon: Object.hasOwn(channelIcons, channelKey) && typeof channelIcons[channelKey] === 'string' ? channelIcons[channelKey] : '',
313
317
  status: statusKey === '' ? '' : this.tr(Object.hasOwn(statusMap, statusKey) ? statusMap[statusKey] : statusKey),
314
318
  actions: this.messageActions
315
319
  .filter((action) => this.actionApplies(action, message))
@@ -327,7 +327,7 @@
327
327
  {{ columnLabel(column) }}
328
328
  </template>
329
329
  </th>
330
- <th class="cn-files-browser__col-actions" scope="col">
330
+ <th class="cn-files-browser__col-actions" scope="col" :style="actionsColumnStyle">
331
331
  <span class="hidden-visually">{{ t('nextcloud-vue', 'Actions') }}</span>
332
332
  </th>
333
333
  </tr>
@@ -1343,6 +1343,8 @@ export default {
1343
1343
 
1344
1344
  data() {
1345
1345
  return {
1346
+ /** The actions column's measured width while a row button stands in it, else null. */
1347
+ actionsColumnWidth: null,
1346
1348
  currentPath: this.rootPath,
1347
1349
  /** The open folder as a node, or null before the first listing. */
1348
1350
  folder: null,
@@ -1402,6 +1404,18 @@ export default {
1402
1404
  },
1403
1405
 
1404
1406
  computed: {
1407
+ /**
1408
+ * The actions column's width. The table lays out fixed, so its columns
1409
+ * take their widths from the head; a row button (Beoordelen) beside the
1410
+ * row menu needs more than the menu's 48px, measured from the first row.
1411
+ *
1412
+ * @return {object} The head cell's style.
1413
+ * @spec openspec/changes/files-actions-column-fits-row-button/specs/files-actions-column-fits-row-button/spec.md#requirement-the-actions-column-fits-a-row-button
1414
+ */
1415
+ actionsColumnStyle() {
1416
+ return this.actionsColumnWidth ? { width: this.actionsColumnWidth + 'px' } : {}
1417
+ },
1418
+
1405
1419
  /**
1406
1420
  * The `dropDetail` text: the string, or the text of the first variant
1407
1421
  * `{ when, text }` whose `when` is `true` or that has none.
@@ -1856,6 +1870,10 @@ export default {
1856
1870
  }
1857
1871
  },
1858
1872
 
1873
+ updated() {
1874
+ this.measureActionsColumn()
1875
+ },
1876
+
1859
1877
  beforeUnmount() {
1860
1878
  this.cnDetailSideColumn?.claim?.(this.$.uid, false)
1861
1879
  for (const channel of FOLDER_CHANGED_CHANNELS) {
@@ -1865,6 +1883,34 @@ export default {
1865
1883
  },
1866
1884
 
1867
1885
  methods: {
1886
+ /**
1887
+ * Measure the first row's actions cell (padding, the row button, the
1888
+ * gap and the menu) and keep it as the column's width, so a fixed
1889
+ * table does not push the button past its card. Without a row button
1890
+ * the column keeps its own width.
1891
+ *
1892
+ * @spec openspec/changes/files-actions-column-fits-row-button/specs/files-actions-column-fits-row-button/spec.md#requirement-the-actions-column-fits-a-row-button
1893
+ */
1894
+ measureActionsColumn() {
1895
+ const cell = this.$el && this.$el.querySelector ? this.$el.querySelector('tbody .cn-files-browser__col-actions') : null
1896
+ const button = cell ? cell.querySelector('.cn-files-browser__row-button') : null
1897
+ if (!cell || !button) {
1898
+ if (this.actionsColumnWidth !== null) {
1899
+ this.actionsColumnWidth = null
1900
+ }
1901
+ return
1902
+ }
1903
+ const style = window.getComputedStyle(cell)
1904
+ const content = [...cell.children].reduce((sum, child) => {
1905
+ const childStyle = window.getComputedStyle(child)
1906
+ return sum + child.offsetWidth + (parseFloat(childStyle.marginInlineStart) || 0) + (parseFloat(childStyle.marginInlineEnd) || 0)
1907
+ }, 0)
1908
+ const width = Math.ceil(content + (parseFloat(style.paddingInlineStart) || 0) + (parseFloat(style.paddingInlineEnd) || 0))
1909
+ if (width > 0 && width !== this.actionsColumnWidth) {
1910
+ this.actionsColumnWidth = width
1911
+ }
1912
+ },
1913
+
1868
1914
  t,
1869
1915
 
1870
1916
  preventMiddleClickAutoscroll,
@@ -3588,7 +3634,6 @@ export default {
3588
3634
 
3589
3635
  .cn-files-browser__row-button {
3590
3636
  display: inline-flex;
3591
- margin-inline-end: 8px;
3592
3637
  vertical-align: middle;
3593
3638
  }
3594
3639
 
@@ -121,7 +121,7 @@ export default {
121
121
  | `icon` | Object\|Function | `null` | MDI icon component shown in the card header |
122
122
  | `objectType` | String | `''` | Registered object type slug used by `objectStore.saveObject()` |
123
123
  | `store` | Object | `null` | Explicit Pinia objectStore instance; auto-detected via Pinia when omitted |
124
- | `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. `format: 'duration'` reads an ISO period in words (`P28D` "4 weken"), and `extendBy` names the object's extension period, read as "4 weken, te verdagen met 2". |
124
+ | `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. `format: 'date'` / `'date-long'` reads a date the board way ("3 okt", "3 okt 2026"). `format: 'duration'` reads an ISO period in words (`P28D` "4 weken"), and `extendBy` names the object's extension period, read as "4 weken, te verdagen met 2". |
125
125
  | `columns` | Number | `3` | Number of CSS grid columns in the widget |
126
126
  | `collapsedFields` (`collapsed-fields`) | Number | `null` | Number of fields shown while collapsed, with a "Show all N fields" button for the rest. `null` fits the cell: the whole rows that fit stay, and the button only appears when the cell overflows |
127
127
  | `hideEmpty` | Boolean | `false` | Hide fields with no value instead of rendering an em dash. For discriminated supertypes (one `ticket` schema holding request/complaint/contactmoment), so the grid is type-aware without the schema enumerating which fields belong to which variant. Read grid only — the field being edited, a field with unsaved changes, and the Edit form stay visible; `false`/`0` are values and are never hidden. |
@@ -438,7 +438,9 @@ import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
438
438
  import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
439
439
  import { normalizeLook } from '../../composables/useLook.js'
440
440
  import { useObjectStore } from '../../store/index.js'
441
+ import { formatBoardDate } from '../../utils/boardDate.js'
441
442
  import { formatTermDuration } from '../../utils/builtInFormatters.js'
443
+ import { parseDateValue } from '../../utils/dateVariant.js'
442
444
  import { foldDottedFields, isDottedKey, spreadDottedFields } from '../../utils/dottedFields.js'
443
445
  import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
444
446
  import { humaniseKey } from '../../utils/humaniseKey.js'
@@ -1105,7 +1107,19 @@ export default {
1105
1107
  // label, beside a list that already read "Ingediend".
1106
1108
  const enumLabel = this.enumDisplayLabel(field, raw)
1107
1109
  const empty = this.emptyValueFor(field, raw)
1108
- if (empty === null && field.format === 'duration' && raw !== null && raw !== undefined && raw !== '') {
1110
+ // Only a format the page's override asks for: a schema `format` keeps its own rendering.
1111
+ const shown = this.resolvedOverrides[field.key]?.format
1112
+ if (empty === null && (shown === 'date' || shown === 'date-long') && raw !== null && raw !== undefined && raw !== '') {
1113
+ // The board's short date ("3 okt"), or with the year always ("3 okt 2026").
1114
+ const date = parseDateValue(raw)
1115
+ if (date !== null) {
1116
+ values[field.key] = shown === 'date-long'
1117
+ ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))
1118
+ : formatBoardDate(date)
1119
+ continue
1120
+ }
1121
+ }
1122
+ if (empty === null && shown === 'duration' && raw !== null && raw !== undefined && raw !== '') {
1109
1123
  // A term in words, with the extension at `extendBy` when the object holds one.
1110
1124
  const extension = typeof field.extendBy === 'string' ? readPath(this.objectData || {}, field.extendBy) : null
1111
1125
  values[field.key] = formatTermDuration(raw, extension)
@@ -4,7 +4,10 @@
4
4
  -->
5
5
  <template>
6
6
  <div class="cn-plan-row" :data-testid="`cn-plan-row-${item.key || item.uuid}`">
7
- <span class="cn-plan-row__name">{{ item.name }}</span>
7
+ <span class="cn-plan-row__text">
8
+ <span class="cn-plan-row__name">{{ item.name }}</span>
9
+ <span v-if="item.description" class="cn-plan-row__meta" data-testid="cn-plan-row-meta">{{ item.description }}</span>
10
+ </span>
8
11
  <span class="cn-plan-row__state" :data-state="item.state">{{ stateLabel(item.state) }}</span>
9
12
  <NcButton v-if="item.state === 'enabled' && item.discretionary"
10
13
  :disabled="busy"
@@ -95,11 +98,23 @@ export default {
95
98
  padding-block: 6px;
96
99
  }
97
100
 
98
- .cn-plan-row__name {
101
+ .cn-plan-row__text {
102
+ display: flex;
99
103
  flex: 1 1 auto;
104
+ flex-direction: column;
105
+ gap: 2px;
106
+ min-width: 0;
107
+ }
108
+
109
+ .cn-plan-row__name {
100
110
  font-weight: 600;
101
111
  }
102
112
 
113
+ .cn-plan-row__meta {
114
+ color: var(--color-text-maxcontrast);
115
+ font-size: 13px;
116
+ }
117
+
103
118
  .cn-plan-row__state {
104
119
  padding: 2px 10px;
105
120
  border-radius: 12px;
@@ -309,6 +309,12 @@
309
309
  :style="{ background: item.dot }"
310
310
  aria-hidden="true"
311
311
  data-testid="cn-record-list-dot" />
312
+ <span v-if="iconTiles"
313
+ class="cn-record-list__icon-tile"
314
+ aria-hidden="true"
315
+ data-testid="cn-record-list-icon-tile">
316
+ <CnIcon :name="item.icon" :size="18" />
317
+ </span>
312
318
  <span class="cn-record-list__main">
313
319
  <span v-if="item.title" class="cn-record-list__title">{{ item.title }}</span>
314
320
  <span v-if="item.meta" class="cn-record-list__meta">{{ item.meta }}</span>
@@ -592,6 +598,7 @@ import { resolveDue } from '../../utils/dueText.js'
592
598
  import { fillCountLabel } from '../../utils/fillCountLabel.js'
593
599
  import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
594
600
  import { unwrapAppConfig } from '../../utils/formatMetric.js'
601
+ import { variantForColor } from '../../utils/headerFieldChips.js'
595
602
  import { initialsOf } from '../../utils/initials.js'
596
603
  import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
597
604
  import { isObjectRowsSpec, objectRowsOf } from '../../utils/objectRows.js'
@@ -1000,6 +1007,17 @@ export default {
1000
1007
  return PRESENTATIONS.includes(value) ? value : 'rows'
1001
1008
  },
1002
1009
 
1010
+ /**
1011
+ * Whether a `rows` row leads with its icon in a round tile
1012
+ * (`iconTile: true` beside `icon`), as DqZaakTaken draws "Wat hierna komt".
1013
+ *
1014
+ * @return {boolean}
1015
+ * @spec openspec/changes/record-list-row-icon-tile/specs/record-list/spec.md#requirement-a-record-list-row-can-lead-with-an-icon-tile
1016
+ */
1017
+ iconTiles() {
1018
+ return this.content?.iconTile === true && this.cardIcons
1019
+ },
1020
+
1003
1021
  /**
1004
1022
  * Whether a `cards` card leads its title with an icon: only when the
1005
1023
  * list declares `icon`, so a card without one has no placeholder.
@@ -2627,7 +2645,11 @@ export default {
2627
2645
  },
2628
2646
 
2629
2647
  /**
2630
- * The row's pill: `{ field, labels?, colorMap?, variant?, hideValues? }`.
2648
+ * The row's pill: `{ field, labels?, colorMap?, colorField?, variant?, variantWhen?, hideValues? }`.
2649
+ * `variantWhen` (`[{ when, variant }]`, the first whose condition holds for the row) wins over `colorMap`.
2650
+ * `colorField` reads the tone from the row (a colour name such as a
2651
+ * status type's `colour`: purple, teal, blue ...) where `colorMap`
2652
+ * names none (DqMijnWerk "In behandeling" in the status's purple).
2631
2653
  *
2632
2654
  * @param {object} row The row.
2633
2655
  * @param {object} [config] A pill config; the widget's `pill` by default.
@@ -2652,7 +2674,14 @@ export default {
2652
2674
  const labels = pill.labels && typeof pill.labels === 'object' ? pill.labels : {}
2653
2675
  const map = pill.colorMap && typeof pill.colorMap === 'object' ? pill.colorMap : {}
2654
2676
  const found = Object.keys(map).find((k) => k.toLowerCase() === key.toLowerCase())
2655
- const variant = found ? map[found] : pill.variant
2677
+ // `variantWhen` colours the pill by the row rather than the value: a
2678
+ // finished case is green whatever its case type calls the final status.
2679
+ const rule = Array.isArray(pill.variantWhen)
2680
+ ? pill.variantWhen.find((entry) => entry && entry.when && evaluateVisibleWhenLocal(entry.when, row || {}))
2681
+ : null
2682
+ const colour = pill.colorField ? readPath(row, pill.colorField) : null
2683
+ const fromRow = typeof colour === 'string' && colour !== '' ? variantForColor(colour) : null
2684
+ const variant = rule ? rule.variant : (found ? map[found] : (fromRow || pill.variant))
2656
2685
  return {
2657
2686
  label: labels[key] ? this.translate(labels[key]) : key,
2658
2687
  variant: isBadgeVariant(variant) ? variant : 'default',
@@ -3585,6 +3614,20 @@ a.cn-record-list__inline-title {
3585
3614
  color: var(--color-primary-element);
3586
3615
  text-decoration: underline;
3587
3616
  }
3617
+
3618
+ /* A row's icon in a 36px round tile on the sunken ground, before the text
3619
+ (`iconTile`, DqZaakTaken "Wat hierna komt"). */
3620
+ .cn-record-list__icon-tile {
3621
+ display: flex;
3622
+ flex: none;
3623
+ align-items: center;
3624
+ justify-content: center;
3625
+ width: 36px;
3626
+ height: 36px;
3627
+ border-radius: 18px;
3628
+ background: var(--color-background-dark);
3629
+ color: var(--color-main-text);
3630
+ }
3588
3631
  </style>
3589
3632
 
3590
3633
  <style>
@@ -658,6 +658,12 @@ export default {
658
658
  flex-direction: column;
659
659
  gap: 2px;
660
660
  min-width: 0;
661
+ line-height: normal;
662
+ }
663
+
664
+ /* A row's second line is 13px, as the boards draw it (DqZaakArchivering). */
665
+ .cn-retention__row-text .cn-retention__muted {
666
+ font-size: 13px;
661
667
  }
662
668
 
663
669
  /* A round mark before a hold or transfer row: LH (legal hold) on the error
@@ -671,7 +677,7 @@ export default {
671
677
  height: 32px;
672
678
  border-radius: 16px;
673
679
  background: var(--color-background-dark);
674
- color: var(--color-text-maxcontrast);
680
+ color: var(--cn-board-text-soft, var(--color-text-maxcontrast));
675
681
  font-size: 12px;
676
682
  font-weight: 700;
677
683
  }
@@ -682,6 +688,7 @@ export default {
682
688
  }
683
689
 
684
690
  .cn-retention__row-title {
691
+ font-size: 15px;
685
692
  font-weight: 600;
686
693
  }
687
694
 
@@ -102,7 +102,7 @@
102
102
  <span class="cn-timeline-manage__author">{{ entry.author }}</span>
103
103
  <span class="cn-timeline-manage__moment">{{ moment(entry.created) }}</span>
104
104
  <span v-if="entry.visibility === 'public'"
105
- class="cn-timeline-manage__badge"
105
+ class="cn-timeline-manage__badge cn-timeline-manage__badge--public"
106
106
  data-testid="cn-timeline-public">
107
107
  {{ publicLabel }}
108
108
  </span>
@@ -463,6 +463,12 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
463
463
  font-size: var(--cn-field-text-size, 15px);
464
464
  }
465
465
 
466
+ /* A board textarea's lines sit on the normal line height, as the boards'
467
+ `font` shorthand sets it: two rows are 58px, not 68 (DqArchiveren Reden). */
468
+ .cn-dialog-board .dialog__content .textarea__input {
469
+ line-height: normal;
470
+ }
471
+
466
472
  /* A plain input an app puts in a board dialog (a date, a number) is the same
467
473
  40px control; Nextcloud's core stylesheet makes it 34px. */
468
474
  .cn-dialog-board .dialog__content input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=range]):not([type=color]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image]):not(.input-field__input) {
@@ -19,6 +19,13 @@
19
19
  --cn-board-notice-ink: var(--color-warning-text, #7a4f00);
20
20
  /* The done marker of the next-step card. */
21
21
  --cn-board-success-tint: var(--color-success-light, #e7f3ea);
22
+ /* The ink on that tint: a completed plan row's state. */
23
+ --cn-board-success-ink: var(--color-success-text, #1e6b2a);
24
+ /* The ink on the light primary fill: an active plan row's state. */
25
+ --cn-board-primary-light-ink: var(--color-primary-element-light-text, #234a78);
26
+ /* The teal badge of a public history entry. */
27
+ --cn-board-teal-tint: var(--nldesign-color-teal-light, #e3f1f3);
28
+ --cn-board-teal-ink: var(--nldesign-color-teal-dark, #1d5e66);
22
29
  /* Card h2 and side heading. */
23
30
  --cn-board-heading-size: 17px;
24
31
  --cn-board-side-heading-size: 15px;
@@ -582,6 +589,20 @@
582
589
  font-size: 13px;
583
590
  }
584
591
 
592
+ /* The initials beside a message: the resident's in the soft ink on the chip
593
+ ground, ours in the primary tint's ink on its light ground, as
594
+ DqZaakContact draws SV and PJ (r31: the initials read main ink).
595
+ @spec openspec/changes/board-conversation-avatar-tones/specs/board-conversation-avatar-tones/spec.md#requirement-a-messages-initials-take-their-sides-tone */
596
+ .cn-look-board .cn-conversation-thread .cn-conversation-thread__avatar:not(.cn-look-nextcloud *) {
597
+ background: var(--color-background-dark);
598
+ color: var(--cn-board-text-soft);
599
+ }
600
+
601
+ .cn-look-board .cn-conversation-thread .cn-conversation-thread__message--us .cn-conversation-thread__avatar:not(.cn-look-nextcloud *) {
602
+ background: var(--color-primary-element-light);
603
+ color: var(--color-primary-element-light-text);
604
+ }
605
+
585
606
  /* A message keeps 64px free on the side away from its avatar, so the two
586
607
  sides read as two columns; the line above it is 13px in the soft ink at the
587
608
  normal line height, and our bubble's text is in the main ink. Each names
@@ -849,6 +870,13 @@
849
870
  font-weight: 500;
850
871
  }
851
872
 
873
+ /* The mail card's header buttons (Mail koppelen, Mail opstellen) are the
874
+ board's 40px control (DqZaakEmail; probed on :8080: 34 live).
875
+ @spec openspec/changes/board-mail-header-buttons-40/specs/board-mail-header-buttons-40/spec.md#requirement-the-mail-cards-header-buttons-are-40px */
876
+ .cn-look-board .cn-mail-tab__header-actions .button-vue:where(:not(.cn-look-nextcloud *)) {
877
+ --button-size: var(--cn-board-control-height);
878
+ }
879
+
852
880
  /* The files table's checkbox column is the board's 42px (12px either side
853
881
  of the 18px box): DqZaakDocumenten and DqDocumentenBeheer both draw it so.
854
882
  The component's 36px pushed the file icon and name 6px left (r31: name 96
@@ -2428,3 +2456,112 @@
2428
2456
  .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *):has(> .cn-widget-wrapper__header .cn-widget-wrapper__subtitle) > .cn-widget-wrapper__content:has(> .cn-object-data-widget__grid-wrap) {
2429
2457
  padding-top: 14px;
2430
2458
  }
2459
+
2460
+ /* Buttons in a side card stand as the boards draw them in a column: the full
2461
+ width of the card with their label at the start (DqSociaalDomein
2462
+ "Toestemming intrekken" and "Markeer voor aandacht"), a facts action at the
2463
+ 34px control height.
2464
+ @spec openspec/changes/board-r31-side-buttons-checklist-boxes/specs/board-r31-side-buttons-checklist-boxes/spec.md#requirement-a-side-cards-button-spans-the-card-with-its-label-at-the-start */
2465
+ .cn-look-board .cn-detail-page__side .cn-facts-widget__actions:not(.cn-look-nextcloud *) > .button-vue {
2466
+ --button-size: 34px;
2467
+ --default-clickable-area: 34px;
2468
+ flex: 1 1 100%;
2469
+ height: 34px;
2470
+ min-height: 34px;
2471
+ }
2472
+
2473
+ .cn-look-board .cn-detail-page__side .cn-facts-widget__actions:not(.cn-look-nextcloud *) > .button-vue .button-vue__wrapper,
2474
+ .cn-look-board .cn-attention-flag:not(.cn-look-nextcloud *) .button-vue .button-vue__wrapper {
2475
+ justify-content: flex-start;
2476
+ }
2477
+
2478
+ /* A checklist's boxes are the boards' 18px, an open one in the control
2479
+ border grey and a ticked one in the primary colour (DqTaak "Checklist").
2480
+ @spec openspec/changes/board-r31-side-buttons-checklist-boxes/specs/board-r31-side-buttons-checklist-boxes/spec.md#requirement-a-board-checklist-box-is-18px-and-grey-while-open */
2481
+ .cn-look-board .cn-checklist-widget__item .checkbox-radio-switch:not(.cn-look-nextcloud *) .checkbox-content__icon svg {
2482
+ width: 18px;
2483
+ height: 18px;
2484
+ }
2485
+
2486
+ .cn-look-board .cn-checklist-widget__item .checkbox-radio-switch:not(.checkbox-radio-switch--checked):not(.cn-look-nextcloud *) .checkbox-content__icon {
2487
+ color: var(--color-border-maxcontrast);
2488
+ }
2489
+
2490
+ /* In a side card the pill and its date stand 8px apart, as the boards draw
2491
+ them (DqOrganisatie "Lopende zaken", "Open tickets"); the row's own 16px
2492
+ is for a full-width list.
2493
+ @spec openspec/changes/record-list-pill-variant-when/specs/record-list/spec.md#requirement-a-side-rows-pill-and-date-stand-8px-apart */
2494
+ .cn-look-board .cn-detail-page__side .cn-record-list__aside:not(.cn-look-nextcloud *) {
2495
+ gap: 8px;
2496
+ }
2497
+
2498
+ /* A plan in the board look, as DqZaak draws "Zaakplan": ruled rows 10px
2499
+ high on each side, the name at 15px/500 over its muted line, the state a
2500
+ 13px/600 pill toned by state (r31: grey rows, 600 names, 400 pills).
2501
+ @spec openspec/changes/board-plan-rows/specs/plan-widget/spec.md */
2502
+ .cn-look-board .cn-plan .cn-plan-row:not(.cn-look-nextcloud *) {
2503
+ gap: 6px 16px;
2504
+ padding: 10px 0;
2505
+ border-top: 1px solid var(--color-border);
2506
+ background: transparent;
2507
+ }
2508
+
2509
+ .cn-look-board .cn-plan .cn-plan-row .cn-plan-row__name:not(.cn-look-nextcloud *) {
2510
+ font-size: 15px;
2511
+ font-weight: 500;
2512
+ line-height: normal;
2513
+ }
2514
+
2515
+ .cn-look-board .cn-plan .cn-plan-row .cn-plan-row__meta:not(.cn-look-nextcloud *) {
2516
+ line-height: normal;
2517
+ }
2518
+
2519
+ .cn-look-board .cn-plan .cn-plan-row .cn-plan-row__state:not(.cn-look-nextcloud *) {
2520
+ padding: 3px 10px;
2521
+ border-radius: 12px;
2522
+ background: var(--color-background-dark);
2523
+ color: var(--cn-board-text-soft);
2524
+ font-size: 13px;
2525
+ font-weight: 600;
2526
+ line-height: normal;
2527
+ white-space: nowrap;
2528
+ }
2529
+
2530
+ .cn-look-board .cn-plan .cn-plan-row .cn-plan-row__state[data-state='completed']:not(.cn-look-nextcloud *) {
2531
+ background: var(--cn-board-success-tint);
2532
+ color: var(--cn-board-success-ink);
2533
+ }
2534
+
2535
+ .cn-look-board .cn-plan .cn-plan-row .cn-plan-row__state[data-state='active']:not(.cn-look-nextcloud *) {
2536
+ background: var(--color-primary-element-light);
2537
+ color: var(--cn-board-primary-light-ink);
2538
+ }
2539
+
2540
+ /* The history card's note composer and entries as DqZaakHistorie draws them
2541
+ (r31 strict verdict): the note field three lines high on a 1.5 line with
2542
+ 10px 12px inside, the add button at the board's 40px, the title-row
2543
+ buttons 12px apart, and an entry the requester sees marked with a 12px
2544
+ teal pill.
2545
+ @spec openspec/changes/board-history-composer/specs/timeline/spec.md */
2546
+ .cn-look-board .cn-timeline-manage .cn-timeline-manage__draft:not(.cn-look-nextcloud *) {
2547
+ min-height: calc(3 * 1.5em + 22px);
2548
+ padding: 10px 12px;
2549
+ line-height: 1.5;
2550
+ }
2551
+
2552
+ .cn-look-board .cn-timeline-manage .cn-timeline-manage__composer-foot .button-vue:where(:not(.cn-look-nextcloud *)) {
2553
+ --button-size: var(--cn-board-control-height);
2554
+ }
2555
+
2556
+ .cn-look-board .cn-timeline-manage .cn-timeline-manage__header-actions:not(.cn-look-nextcloud *) {
2557
+ gap: 12px;
2558
+ }
2559
+
2560
+ .cn-look-board .cn-timeline-manage .cn-timeline-manage__badge--public:not(.cn-look-nextcloud *) {
2561
+ padding: 2px 8px;
2562
+ border-radius: 10px;
2563
+ background: var(--cn-board-teal-tint);
2564
+ color: var(--cn-board-teal-ink);
2565
+ font-size: 12px;
2566
+ font-weight: 600;
2567
+ }