@conduction/nextcloud-vue 2.129.0 → 2.130.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 (63) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +87 -4
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  6. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +29 -3
  7. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  8. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +68 -14
  9. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  10. package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
  11. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +16 -5
  12. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
  13. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
  14. package/dist/esm/components/CnFilesCard/CnFilesCard.vue.js +1 -1
  15. package/dist/esm/components/CnFilesCard/CnFilesCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnFilesCard/CnFilesCard.vue3.js +11 -2
  17. package/dist/esm/components/CnFilesCard/CnFilesCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIcon/semanticIcons.js +2 -0
  19. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +32 -2
  21. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +3 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  25. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  26. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  27. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.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 +62 -2
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +515 -356
  32. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  33. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  34. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +6 -1
  35. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  36. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +17 -9
  37. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  38. package/dist/esm/library-style.js +1 -1
  39. package/dist/esm/nextcloud-vue.css +323 -165
  40. package/dist/esm/utils/boardReferenceColumns.js +38 -1
  41. package/dist/esm/utils/boardReferenceColumns.js.map +1 -1
  42. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  43. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  44. package/dist/nextcloud-vue.cjs.js +989 -479
  45. package/dist/nextcloud-vue.cjs.js.map +2 -2
  46. package/dist/nextcloud-vue.css +323 -165
  47. package/package.json +1 -1
  48. package/src/components/CnActionsBar/CnActionsBar.vue +67 -3
  49. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +59 -8
  50. package/src/components/CnEventsCard/CnEventsCard.vue +16 -5
  51. package/src/components/CnFilesCard/CnFilesCard.vue +7 -0
  52. package/src/components/CnIcon/semanticIcons.js +2 -0
  53. package/src/components/CnIndexPage/CnIndexPage.vue +34 -1
  54. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  55. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +186 -8
  56. package/src/css/look-board-detail.css +27 -2
  57. package/src/css/look-board-index.css +50 -2
  58. package/src/css/look-board.css +52 -2
  59. package/src/integrations/builtin/calendar/CnCalendarCard.vue +12 -3
  60. package/src/integrations/builtin/calendar/__tests__/CnCalendarCardHeaderActions.spec.js +12 -1
  61. package/src/schemas/app-manifest-v2.schema.json +27 -3
  62. package/src/utils/boardReferenceColumns.js +37 -0
  63. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -64,6 +64,9 @@
64
64
  {{ col.label }}
65
65
  </template>
66
66
  </th>
67
+ <th v-if="rowActions.length > 0" scope="col" class="cn-record-list__cell--actions">
68
+ {{ rowActionsHeader }}
69
+ </th>
67
70
  </tr>
68
71
  </thead>
69
72
  <tbody>
@@ -78,12 +81,34 @@
78
81
  :label="cell.text"
79
82
  :variant="cell.tone"
80
83
  size="small" />
81
- <router-link v-else-if="cellIndex === 0 && item.to" :to="item.to">
84
+ <router-link v-else-if="cellIndex === 0 && item.to" :to="item.to" class="cn-record-list__cell-text">
82
85
  {{ cell.text }}
83
86
  </router-link>
84
- <template v-else>
85
- {{ cell.text }}
86
- </template>
87
+ <span v-else class="cn-record-list__cell-text">{{ cell.text }}</span>
88
+ <span v-if="cell.sub" class="cn-record-list__cell-sub">{{ cell.sub }}</span>
89
+ </td>
90
+ <td v-if="rowActions.length > 0" class="cn-record-list__cell--actions">
91
+ <NcActions
92
+ v-if="item.rowActions.length > 0"
93
+ :aria-label="rowActionsLabel"
94
+ data-testid="cn-record-list-row-actions">
95
+ <template v-for="entry in item.rowActions" :key="`row-action-${entry.index}`">
96
+ <NcActionLink
97
+ v-if="entry.href"
98
+ :href="entry.href"
99
+ :target="entry.external ? '_blank' : undefined"
100
+ :data-testid="`cn-record-list-row-action-${entry.index}`">
101
+ {{ entry.action.label }}
102
+ </NcActionLink>
103
+ <NcActionButton
104
+ v-else
105
+ :closeAfterClick="true"
106
+ :data-testid="`cn-record-list-row-action-${entry.index}`"
107
+ @click="runRowActionEntry(entry.action, item.row)">
108
+ {{ entry.action.label }}
109
+ </NcActionButton>
110
+ </template>
111
+ </NcActions>
87
112
  </td>
88
113
  </tr>
89
114
  </tbody>
@@ -99,14 +124,38 @@
99
124
  <!-- `groupBy`: a heading per group, each group its own list, named by
100
125
  its heading. Without it this is one list, as before. -->
101
126
  <template v-for="group in itemGroups" v-else :key="group.key">
127
+ <div
128
+ v-if="group.label && footerGroup !== '' && group.value === footerGroup"
129
+ class="cn-record-list__group-head"
130
+ data-testid="cn-record-list-group-head">
131
+ <h3
132
+ :id="group.headingId"
133
+ class="cn-record-list__group-heading"
134
+ data-testid="cn-record-list-group-heading">
135
+ {{ group.label }}
136
+ </h3>
137
+ <NcButton
138
+ variant="secondary"
139
+ :class="{ 'cn-record-list__action--tonal': footerTonal }"
140
+ :disabled="footerDisabled"
141
+ :aria-describedby="footerDisabled && footerAction.reason ? footerReasonId : null"
142
+ data-testid="cn-record-list-footer-action"
143
+ @click="runFooterAction">
144
+ <template v-if="footerAction.icon" #icon>
145
+ <CnIcon :name="footerAction.icon" :size="16" />
146
+ </template>
147
+ {{ footerAction.label }}
148
+ </NcButton>
149
+ </div>
102
150
  <h3
103
- v-if="group.label"
151
+ v-else-if="group.label"
104
152
  :id="group.headingId"
105
153
  class="cn-record-list__group-heading"
106
154
  data-testid="cn-record-list-group-heading">
107
155
  {{ group.label }}
108
156
  </h3>
109
157
  <ul
158
+ v-if="group.items.length > 0"
110
159
  class="cn-record-list__list"
111
160
  :aria-labelledby="group.label ? group.headingId : null"
112
161
  :data-testid="isGrouped ? 'cn-record-list-group' : null">
@@ -360,11 +409,39 @@
360
409
  says why (`disabledReason`: "Kan zodra de beslissing is
361
410
  verzonden"). -->
362
411
  <div
363
- v-if="footerAction && !error"
412
+ v-if="footerGroupHead && rows.length === 0 && !error && !loading"
413
+ class="cn-record-list__group-head"
414
+ data-testid="cn-record-list-group-head">
415
+ <h3 class="cn-record-list__group-heading" data-testid="cn-record-list-group-heading">
416
+ {{ footerGroupHead }}
417
+ </h3>
418
+ <NcButton
419
+ variant="secondary"
420
+ :class="{ 'cn-record-list__action--tonal': footerTonal }"
421
+ :disabled="footerDisabled"
422
+ :aria-describedby="footerDisabled && footerAction.reason ? footerReasonId : null"
423
+ data-testid="cn-record-list-footer-action"
424
+ @click="runFooterAction">
425
+ <template v-if="footerAction.icon" #icon>
426
+ <CnIcon :name="footerAction.icon" :size="16" />
427
+ </template>
428
+ {{ footerAction.label }}
429
+ </NcButton>
430
+ </div>
431
+ <p
432
+ v-if="footerGroup !== '' && footerDisabled && footerAction && footerAction.reason && !error"
433
+ :id="footerReasonId"
434
+ class="cn-record-list__footer-reason"
435
+ data-testid="cn-record-list-footer-reason">
436
+ {{ footerAction.reason }}
437
+ </p>
438
+ <div
439
+ v-if="footerAction && footerGroup === '' && !error"
364
440
  class="cn-record-list__footer"
365
441
  data-testid="cn-record-list-footer">
366
442
  <NcButton
367
443
  variant="secondary"
444
+ :class="{ 'cn-record-list__action--tonal': footerTonal }"
368
445
  :disabled="footerDisabled"
369
446
  :aria-describedby="footerDisabled && footerAction.reason ? footerReasonId : null"
370
447
  data-testid="cn-record-list-footer-action"
@@ -661,6 +738,41 @@ export default {
661
738
  }
662
739
  },
663
740
 
741
+ /**
742
+ * The group value `footerAction.group` puts the action in, with
743
+ * `groupBy`; '' when the action stands under the list.
744
+ *
745
+ * @spec openspec/changes/record-list-group-action/specs/record-list/spec.md#requirement-a-record-lists-action-can-end-a-group-heading-as-a-tonal-pill
746
+ * @return {string}
747
+ */
748
+ footerGroup() {
749
+ const group = this.content?.footerAction?.group
750
+ if (!this.footerAction || !this.isGrouped || group === null || group === undefined || group === '') {
751
+ return ''
752
+ }
753
+ return String(group)
754
+ },
755
+
756
+ /**
757
+ * The heading of the action's group, for the list without rows.
758
+ *
759
+ * @return {string}
760
+ */
761
+ footerGroupHead() {
762
+ if (this.footerGroup === '') {
763
+ return ''
764
+ }
765
+ const c = this.content
766
+ const labels = c.valueLabels && typeof c.valueLabels[c.groupBy] === 'object' ? c.valueLabels[c.groupBy] : {}
767
+ const named = labels[this.footerGroup] || this.footerGroup
768
+ return this.translate(named)
769
+ },
770
+
771
+ /** @return {boolean} Whether the action is drawn as a tonal pill (`footerAction.variant: "tonal"`). */
772
+ footerTonal() {
773
+ return this.content?.footerAction?.variant === 'tonal'
774
+ },
775
+
664
776
  /**
665
777
  * What `footerAction.disabledWhen` depends on, so it is evaluated again
666
778
  * when the condition, the record or the workspace changes.
@@ -842,7 +954,7 @@ export default {
842
954
  * or not at all. Without `groupBy` all rows are one unheaded group.
843
955
  *
844
956
  * @spec openspec/changes/record-list-group-headings/specs/record-list/spec.md#requirement-a-record-list-can-group-its-rows-under-headings
845
- * @return {Array<{key: string, label: string, headingId: string, items: Array<object>}>} The groups.
957
+ * @return {Array<{key: string, value: string, label: string, headingId: string, items: Array<object>}>} The groups.
846
958
  */
847
959
  itemGroups() {
848
960
  if (!this.isGrouped) {
@@ -869,10 +981,15 @@ export default {
869
981
  if (buckets.has('')) {
870
982
  values.push('')
871
983
  }
984
+ if (this.footerGroup !== '' && !buckets.has(this.footerGroup)) {
985
+ buckets.set(this.footerGroup, [])
986
+ values.splice(buckets.has('') ? values.length - 1 : values.length, 0, this.footerGroup)
987
+ }
872
988
  return values.map((value, index) => {
873
989
  const named = value === '' ? c.groupEmptyLabel : (labels[value] || value)
874
990
  return {
875
991
  key: `group-${value}`,
992
+ value,
876
993
  label: typeof named === 'string' && named !== '' ? this.translate(named) : '',
877
994
  headingId: `${this.groupIdBase}-${index}`,
878
995
  items: buckets.get(value),
@@ -904,6 +1021,8 @@ export default {
904
1021
  const indentPath = this.content?.indentPath
905
1022
  const rows = this.items.map((item) => ({
906
1023
  key: item.key,
1024
+ row: item.row,
1025
+ rowActions: this.rowActions.length > 0 ? this.rowActionsOf(item.row) : [],
907
1026
  to: item.attrs.to || null,
908
1027
  depth: indentPath ? Math.max(0, Math.floor(Number(readPath(item.row, indentPath)) || 0)) : 0,
909
1028
  cells: this.tableColumns.map((col) => {
@@ -913,6 +1032,7 @@ export default {
913
1032
  return {
914
1033
  key: col.key,
915
1034
  text,
1035
+ sub: typeof col.sub === 'string' ? this.fill(col.sub, item.row) : '',
916
1036
  align: col.align,
917
1037
  alert: col.alertAbove !== undefined && Number.isFinite(limit) && text !== '' && Number.isFinite(value) && value > limit,
918
1038
  tone: this.cellToneOf(col, item.row, text),
@@ -1110,6 +1230,17 @@ export default {
1110
1230
  }))
1111
1231
  },
1112
1232
 
1233
+ /**
1234
+ * The heading of the `table` presentation's actions column.
1235
+ *
1236
+ * @return {string}
1237
+ * @spec openspec/changes/record-list-table-row-actions/specs/record-list-table/spec.md#requirement-a-table-row-can-carry-a-second-line-and-an-actions-menu
1238
+ */
1239
+ rowActionsHeader() {
1240
+ const own = this.content?.rowActionsHeader
1241
+ return typeof own === 'string' && own !== '' ? this.translate(own) : t('nextcloud-vue', 'Actions')
1242
+ },
1243
+
1113
1244
  /** @return {string} The accessible name of a row's actions menu. */
1114
1245
  rowActionsLabel() {
1115
1246
  const own = this.content?.rowActionsLabel
@@ -1318,7 +1449,7 @@ export default {
1318
1449
  if (!declared || this.footerDisabled) {
1319
1450
  return undefined
1320
1451
  }
1321
- const { disabledWhen, disabledReason, ...action } = declared
1452
+ const { disabledWhen, disabledReason, group, variant, ...action } = declared
1322
1453
  if (typeof this.cnDispatchAction === 'function') {
1323
1454
  return this.cnDispatchAction(action)
1324
1455
  }
@@ -1531,16 +1662,22 @@ export default {
1531
1662
  * Run a `rowAction` that is not a plain link through the page's
1532
1663
  * dispatcher: a `handler` gets the row as its last argument, an
1533
1664
  * `open-modal` gets it as the `row` prop, an `object-op` acts on it.
1665
+ * With `endpoint` it calls that app URL for the row, as a `rowActions`
1666
+ * endpoint entry does.
1534
1667
  *
1535
1668
  * @param {object} row The row.
1536
1669
  * @return {unknown} What the dispatcher returns.
1537
1670
  * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md
1671
+ * @spec openspec/changes/record-list-row-action-endpoint/specs/record-list/spec.md#requirement-a-rows-one-action-can-call-an-app-endpoint-for-that-row
1538
1672
  */
1539
1673
  runRowAction(row) {
1540
1674
  const action = this.content?.rowAction
1541
1675
  if (!action || typeof action !== 'object') {
1542
1676
  return undefined
1543
1677
  }
1678
+ if (typeof action.endpoint === 'string' && action.endpoint !== '') {
1679
+ return this.runRowActionEntry(action, row)
1680
+ }
1544
1681
  const type = action.type || 'handler'
1545
1682
  let wrapped = action
1546
1683
  if (type === 'handler') {
@@ -2342,6 +2479,34 @@ export default {
2342
2479
  line-height: 1.4;
2343
2480
  }
2344
2481
 
2482
+ .cn-record-list__group-head {
2483
+ display: flex;
2484
+ align-items: center;
2485
+ justify-content: space-between;
2486
+ gap: 8px;
2487
+ margin-top: 6px;
2488
+ }
2489
+
2490
+ .cn-record-list__group-head .cn-record-list__group-heading {
2491
+ margin: 0;
2492
+ }
2493
+
2494
+ .cn-record-list__action--tonal.button-vue {
2495
+ min-height: 34px;
2496
+ height: 34px;
2497
+ padding: 0 12px;
2498
+ border: 0;
2499
+ border-radius: 17px;
2500
+ background-color: var(--color-primary-element-light);
2501
+ color: var(--color-primary-element-light-text);
2502
+ font-size: 13px;
2503
+ font-weight: 600;
2504
+ }
2505
+
2506
+ .cn-record-list__action--tonal.button-vue:hover:not(:disabled) {
2507
+ background-color: var(--color-primary-element-light-hover);
2508
+ }
2509
+
2345
2510
  .cn-record-list__footer {
2346
2511
  display: flex;
2347
2512
  flex-direction: column;
@@ -2539,6 +2704,19 @@ export default {
2539
2704
  border-bottom: 1px solid var(--color-border);
2540
2705
  }
2541
2706
 
2707
+ .cn-record-list__cell-sub {
2708
+ display: block;
2709
+ color: var(--color-text-maxcontrast);
2710
+ font-size: 13px;
2711
+ }
2712
+
2713
+ .cn-record-list__table .cn-record-list__cell--actions {
2714
+ width: 1%;
2715
+ padding-inline-start: 12px;
2716
+ text-align: end;
2717
+ white-space: nowrap;
2718
+ }
2719
+
2542
2720
  .cn-record-list__sort {
2543
2721
  display: inline-flex;
2544
2722
  gap: 4px;
@@ -562,14 +562,17 @@
562
562
  font-size: 13px;
563
563
  }
564
564
 
565
- .cn-look-board .cn-conversation-thread__channel {
565
+ /* A message's channel pill: 20px in the soft ink (DqZaakContact). It names
566
+ the thread to outrank the component's scoped muted ink. */
567
+ .cn-look-board .cn-conversation-thread .cn-conversation-thread__channel {
566
568
  padding: 1px 8px;
567
569
  border: 1px solid var(--color-border);
568
570
  border-radius: 12px;
569
571
  background: var(--color-main-background);
570
- color: var(--color-main-text);
572
+ color: var(--cn-board-text-soft);
571
573
  font-size: 12px;
572
574
  font-weight: 600;
575
+ line-height: 16px;
573
576
  }
574
577
 
575
578
  /* The case Status card as the board draws it: no grey box, the status's
@@ -1324,3 +1327,25 @@
1324
1327
  .cn-look-board .cn-facts-widget__action:not(.cn-look-nextcloud *) {
1325
1328
  font-weight: 600;
1326
1329
  }
1330
+
1331
+ /* ---- A card's Add ------------------------------------------------------ */
1332
+ /* The board draws a list card's Add as the neutral secondary at the 40px
1333
+ control height, right-aligned in the title row (DqZaakGerelateerd
1334
+ "Deelzaak aanmaken", DqZaakTaken "Toevoegen"). CnDetailWidgetHost renders
1335
+ that button in the board look, so the list's footer Add bar, which ADR-062
1336
+ draws in the Nextcloud look, is hidden in the card. The size outranks a
1337
+ card title row's 34px buttons.
1338
+ @spec openspec/changes/board-object-list-add-button/specs/board-object-list-add-button/spec.md#requirement-the-board-look-draws-a-cards-add-as-a-title-row-button */
1339
+ .cn-look-board .cn-detail-page__catalog-card:not(.cn-look-nextcloud *) .cn-object-list-widget__add {
1340
+ display: none;
1341
+ }
1342
+
1343
+ /* An empty list card shows its text under the title-row Add, without the
1344
+ empty state's own Add. A filter's "clear" action stays. */
1345
+ .cn-look-board .cn-detail-page__catalog-card:not(.cn-look-nextcloud *) .cn-widget-empty-state__action:has(> .cn-object-list-widget__empty-add) {
1346
+ display: none;
1347
+ }
1348
+
1349
+ .cn-look-board .cn-widget-wrapper__actions .button-vue.cn-detail-page__catalog-add:has(> .button-vue__wrapper > .button-vue__text:not(:empty)):where(:not(.cn-look-nextcloud *)) {
1350
+ --button-size: var(--cn-board-control-height);
1351
+ }
@@ -208,6 +208,16 @@
208
208
  cursor: pointer;
209
209
  }
210
210
 
211
+ /* Inside a record card the chip row is 36px, 12px in (DqZaakContact's channel
212
+ chips, as DqZaakHistorie's). */
213
+ .cn-look-board .cn-conversation-thread .cn-quick-filter-bar .cn-quick-filter-bar__tab {
214
+ --nldesign-component-button-border-radius: 18px;
215
+ height: 36px;
216
+ min-height: 36px;
217
+ padding: 0 12px;
218
+ border-radius: 18px;
219
+ }
220
+
211
221
  .cn-look-board .cn-saved-views__chips .cn-saved-views__chip:hover,
212
222
  .cn-look-board .cn-quick-filter-bar .cn-quick-filter-bar__tab:hover {
213
223
  background: var(--color-background-hover);
@@ -805,8 +815,13 @@
805
815
  padding: 0;
806
816
  }
807
817
 
808
- /* The title column: a 15px weight 600 link, the secondary line under it. */
809
- .cn-look-board .cn-table-container--board .cn-data-table td.cn-table-col--title {
818
+ /* The title column: a 15px weight 600 link, the secondary line under it. A
819
+ framed widget table in a card or a settings section shows its first column
820
+ as plain text (DqInstellingen, DqTermijnen, DqDoorlooptijd); a borderless
821
+ one, the dashboard's compact case list, keeps the link (DqDashboard). The
822
+ `:where()` keeps the rule's weight.
823
+ @spec openspec/changes/board-widget-table-plain-title/specs/board-widget-table-plain-title/spec.md */
824
+ .cn-look-board .cn-table-container--board .cn-data-table td.cn-table-col--title:not(.cn-widget-object-table :where(.cn-table-container:not(.cn-table-container--borderless)) *) {
810
825
  color: var(--color-main-text);
811
826
  font-size: 15px;
812
827
  font-weight: 600;
@@ -1769,3 +1784,36 @@
1769
1784
  .cn-look-board .cn-saved-views-panel__row .action-item__menutoggle:not(.cn-look-nextcloud *) {
1770
1785
  border-radius: 17px;
1771
1786
  }
1787
+
1788
+ /* The board view's selects take the board control border (DqWerkbord). The
1789
+ theme forces a select's border colour from its form token, so the token
1790
+ gets the colour too. */
1791
+ .cn-look-board .cn-index-page__board-select:not(.cn-look-nextcloud *) {
1792
+ --nldesign-component-form-select-border-color: var(--cn-board-control-border);
1793
+ border-color: var(--cn-board-control-border);
1794
+ }
1795
+
1796
+ /* ---- Page tabs (CnIndexPage `pageTabs`, DqContracten) ------------------ */
1797
+ /* The sibling-page tabs are links; the board draws them as folder tabs in
1798
+ the main ink at 14px, the open one at 700 on a white bottom edge, with no
1799
+ line under the strip. The theme paints every link, so the ink also goes
1800
+ to its link tokens and the underline needs !important.
1801
+ @spec openspec/changes/board-index-page-tabs/specs/board-index-page-tabs/spec.md#requirement-board-index-page-tabs-are-folder-tabs-in-the-main-ink */
1802
+ .cn-look-board .cn-index-page__page-tabs:not(.cn-look-nextcloud *) {
1803
+ border-bottom: 0;
1804
+ }
1805
+
1806
+ .cn-look-board .cn-index-page__page-tabs:not(.cn-look-nextcloud *) .cn-index-page__page-tab {
1807
+ --nldesign-color-on-surface: var(--color-main-text);
1808
+ --nldesign-component-link-color: var(--color-main-text);
1809
+ --nldesign-component-link-hover-color: var(--color-main-text);
1810
+ color: var(--color-main-text);
1811
+ font-size: 14px;
1812
+ line-height: normal;
1813
+ text-decoration: none !important;
1814
+ }
1815
+
1816
+ .cn-look-board .cn-index-page__page-tabs:not(.cn-look-nextcloud *) .cn-index-page__page-tab--active {
1817
+ border-bottom: 1px solid var(--color-main-background);
1818
+ font-weight: 700;
1819
+ }
@@ -670,6 +670,13 @@
670
670
  --button-size: var(--cn-board-control-height);
671
671
  }
672
672
 
673
+ /* The contact thread's labelled header action (DqZaakContact "Contact
674
+ vastleggen") stands at the same 40px.
675
+ @spec openspec/changes/board-thread-header-button-height/specs/board-thread-header-button-height/spec.md#requirement-a-thread-headers-labelled-button-is-40px */
676
+ .cn-look-board .cn-conversation-thread__header-actions .button-vue:not(.action-item__menutoggle):has(> .button-vue__wrapper > .button-vue__text:not(:empty)):where(:not(.cn-look-nextcloud *)) {
677
+ --button-size: var(--cn-board-control-height);
678
+ }
679
+
673
680
  /* ---- A workspace filter's table export ---------------------------------- */
674
681
  /* "CSV exporteren" beside "Rapport laden" is the neutral secondary at the
675
682
  40px control height, 14px in (DqTermijnen).
@@ -2350,6 +2357,48 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2350
2357
  color: var(--color-text-maxcontrast) !important;
2351
2358
  }
2352
2359
 
2360
+ /* A text card sits at the card inset with its text 8px under the title
2361
+ (DqDagAfsluiten "Hoe dit werkt"), its blocks at the widget's size and a
2362
+ 1.5 line height. CnTextWidget pads itself 16px inline and the theme sets
2363
+ every p at 15px, hence the !important.
2364
+ @spec openspec/changes/board-dashboard-text-card/specs/board-dashboard-text-card/spec.md#requirement-a-board-dashboard-text-card-sits-at-the-card-inset-under-its-title */
2365
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-text-widget {
2366
+ margin-top: -2px;
2367
+ padding: 0 var(--cn-board-dashboard-card-inset) 20px !important;
2368
+ }
2369
+
2370
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-text-widget__content > * {
2371
+ font-size: inherit !important;
2372
+ line-height: 1.5 !important;
2373
+ }
2374
+
2375
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-text-widget__content > :first-child {
2376
+ margin-top: 0;
2377
+ }
2378
+
2379
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-text-widget__content > :last-child {
2380
+ margin-bottom: 0;
2381
+ }
2382
+
2383
+ /* A stat grid card draws plain figures at the card inset (DqDagAfsluiten
2384
+ "Uw dag"): 14px under the title, 16px apart, each a 13px label over a 24px
2385
+ figure, 2px apart, without the stat tile's padding or edge. The cell's
2386
+ variables are set on the cell, over a board tile's own.
2387
+ @spec openspec/changes/board-stat-grid-flat/specs/board-stat-grid-flat/spec.md#requirement-a-board-stat-grid-draws-flat-figures-at-the-card-inset */
2388
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-stat-grid {
2389
+ gap: 16px;
2390
+ margin: 4px 0 0;
2391
+ padding: 0 var(--cn-board-dashboard-card-inset) 20px;
2392
+ }
2393
+
2394
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-stat-grid .cn-kpi-card.cn-kpi-card--stacked {
2395
+ --cn-kpi-stacked-padding: 0;
2396
+ --cn-kpi-stacked-gap: 2px;
2397
+ --cn-kpi-stacked-title-size: 13px;
2398
+ --cn-kpi-stacked-value-size: 24px;
2399
+ border-width: 0;
2400
+ }
2401
+
2353
2402
  /* An activity item pads itself 4px, so the body takes 4px less. */
2354
2403
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-nc-widget-widget__body {
2355
2404
  padding: 0 calc(var(--cn-board-dashboard-card-inset) - 4px) 16px;
@@ -2646,9 +2695,10 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2646
2695
  /* Any labelled button in the card's body is 40px, a text-only one 16px in at
2647
2696
  both sides (DqBeheerinstellingen "Opslaan", "Verbinding testen";
2648
2697
  PtBeheerinstellingen "Adres opslaan"), not only the card's own save row.
2649
- The title row's actions keep 34px (DqVthInstellingen "Regel toevoegen").
2698
+ The title row's actions and an image field's button keep 34px
2699
+ (DqVthInstellingen "Regel toevoegen", DqInstellingenAlgemeen "Ander logo").
2650
2700
  @spec openspec/changes/board-settings-section-buttons/specs/board-settings-section-buttons/spec.md#requirement-a-labelled-button-on-a-board-settings-card-is-40px */
2651
- .cn-look-board .settings-section:not(.cn-look-nextcloud *) .button-vue.button-vue:has(> .button-vue__wrapper > .button-vue__text:not(:empty)):not(.cn-settings-section__actions *) {
2701
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .button-vue.button-vue:has(> .button-vue__wrapper > .button-vue__text:not(:empty)):not(.cn-settings-section__actions *):not(.cn-settings-field__image-row *) {
2652
2702
  --button-size: var(--cn-field-height, 40px);
2653
2703
  }
2654
2704
 
@@ -34,11 +34,15 @@
34
34
  <NcButton
35
35
  v-for="action in shownHeaderActions"
36
36
  :key="action"
37
- :variant="action === 'create' ? 'primary' : 'secondary'"
37
+ :variant="action === 'create' && createVariant !== 'secondary' ? 'primary' : 'secondary'"
38
38
  :data-testid="`cn-calendar-card-${action}`"
39
39
  @click="action === 'create' ? (showCreate = true) : (showPicker = true)">
40
40
  <template #icon>
41
- <Plus v-if="action === 'create'" :size="20" />
41
+ <CnIcon v-if="action === 'create' && createIcon"
42
+ :name="createIcon"
43
+ :size="20"
44
+ data-testid="cn-calendar-card-create-icon" />
45
+ <Plus v-else-if="action === 'create'" :size="20" />
42
46
  <LinkVariant v-else :size="20" />
43
47
  </template>
44
48
  {{ action === 'create' ? createLabel : linkLabel }}
@@ -139,6 +143,7 @@ import Plus from 'vue-material-design-icons/Plus.vue'
139
143
  import CnCalendarEventCreate from '../../../components/CnCalendarEventCreate/CnCalendarEventCreate.vue'
140
144
  import CnCalendarEventPicker from '../../../components/CnCalendarEventPicker/CnCalendarEventPicker.vue'
141
145
  import CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'
146
+ import CnIcon from '../../../components/CnIcon/CnIcon.vue'
142
147
  import { buildHeaders, prefixUrl } from '../../../utils/index.js'
143
148
  import { userLocale } from '../../../utils/userLocale.js'
144
149
  import { linkCalendarEvent } from './calendarEventLink.js'
@@ -151,7 +156,7 @@ import { linkCalendarEvent } from './calendarEventLink.js'
151
156
  export default {
152
157
  name: 'CnCalendarCard',
153
158
 
154
- components: { CnCalendarEventCreate, CnCalendarEventPicker, CnDetailCard, LinkVariant, NcButton, NcLoadingIcon, Plus, CalendarClock, CalendarCheck },
159
+ components: { CnCalendarEventCreate, CnCalendarEventPicker, CnDetailCard, CnIcon, LinkVariant, NcButton, NcLoadingIcon, Plus, CalendarClock, CalendarCheck },
155
160
 
156
161
  props: {
157
162
  /** OpenRegister register id (slug or uuid). Optional for user-dashboard. */
@@ -200,6 +205,10 @@ export default {
200
205
  headerActions: { type: Array, default: () => [] },
201
206
  /** Label of the `create` button. */
202
207
  createLabel: { type: String, default: () => t('nextcloud-vue', 'Make an appointment') },
208
+ /** `secondary` draws the `create` button outlined instead of primary. */
209
+ createVariant: { type: String, default: 'primary' },
210
+ /** Icon name of the `create` button; a plus without it. */
211
+ createIcon: { type: String, default: '' },
203
212
  /** Label of the `link` button. */
204
213
  linkLabel: { type: String, default: () => t('nextcloud-vue', 'Link an existing one') },
205
214
  },
@@ -17,7 +17,8 @@ const CnCalendarCard = require('../CnCalendarCard.vue').default
17
17
 
18
18
  const STUBS = {
19
19
  NcLoadingIcon: { template: '<div class="cn-loading" />' },
20
- NcButton: { template: '<button v-bind="$attrs"><slot /></button>' },
20
+ NcButton: { template: '<button v-bind="$attrs"><slot /><slot name="icon" /></button>' },
21
+ CnIcon: { props: ['name'], template: '<i :data-name="name" />' },
21
22
  CnCalendarEventCreate: { name: 'CnCalendarEventCreate', template: '<div class="create-modal" />' },
22
23
  CnCalendarEventPicker: { name: 'CnCalendarEventPicker', template: '<div class="picker-modal" />' },
23
24
  }
@@ -95,4 +96,14 @@ describe('CnCalendarCard header actions', () => {
95
96
  await flush()
96
97
  expect(wrapper.find('[role="alert"]').text()).toContain('Not your calendar')
97
98
  })
99
+
100
+ it('draws the create button secondary when asked', async () => {
101
+ const primary = mountCard({ headerActions: ['create'] })
102
+ await flush()
103
+ expect(primary.find('[data-testid="cn-calendar-card-create"]').attributes('variant')).toBe('primary')
104
+ const quiet = mountCard({ headerActions: ['create'], createVariant: 'secondary', createIcon: 'CalendarPlusOutline' })
105
+ await flush()
106
+ expect(quiet.find('[data-testid="cn-calendar-card-create"]').attributes('variant')).toBe('secondary')
107
+ expect(quiet.find('[data-testid="cn-calendar-card-create-icon"]').attributes('data-name')).toBe('CalendarPlusOutline')
108
+ })
98
109
  })
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.149.0",
6
+ "version": "2.150.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -1346,7 +1346,8 @@
1346
1346
  },
1347
1347
  "readOnly": { "type": "boolean", "default": false, "description": "No drag and no Move to while the columns come from this field." },
1348
1348
  "moveHandler": { "type": "string", "minLength": 1, "description": "A handler in the app's registry that makes the move: it gets `{ card, toKey, field, value, target }` (value: the referenced id to write; target: its row) and returns a promise; a rejection shows its message as the refusal. Absent, the field is written with a PATCH." },
1349
- "moveTargetsHandler": { "type": "string", "minLength": 1, "description": "A handler in the app's registry that answers where one card may go: it gets `{ card, field }` and returns the values (referenced ids) it can reach. Columns out of reach fade during a drag, read \"not allowed\" in the card menu, and refuse a drop." }
1349
+ "moveTargetsHandler": { "type": "string", "minLength": 1, "description": "A handler in the app's registry that answers where one card may go: it gets `{ card, field }` and returns the values (referenced ids) it can reach. Columns out of reach fade during a drag, read \"not allowed\" in the card menu, and refuse a drop." },
1350
+ "states": { "$ref": "#/$defs/boardDeclaredStates" }
1350
1351
  }
1351
1352
  }
1352
1353
  },
@@ -1394,7 +1395,30 @@
1394
1395
  "type": "boolean",
1395
1396
  "default": false,
1396
1397
  "description": "screens-board-reference-columns (2.97.0): wrap the columns onto more rows, as many to a row as fit at 240px, instead of one row that scrolls sideways. For a board with more stages than fit."
1397
- }
1398
+ },
1399
+ "states": { "$ref": "#/$defs/boardDeclaredStates" }
1400
+ }
1401
+ },
1402
+ "boardDeclaredStates": {
1403
+ "type": "array",
1404
+ "minItems": 1,
1405
+ "description": "board-declared-states (2.150.0): the columns the board draws for a field, in order, instead of the field's enum or lifecycle states. Each entry is a value or `{ key, label?, color?, hidden?, closed? }`: `hidden` draws no column (its cards go to Elsewhere), `closed` starts the column folded, `label` is an i18n key. On `board` it applies to the `statusField`; on a `columnFields` entry to that field. Ignored on a `source` or `kind: values` column.",
1406
+ "items": {
1407
+ "oneOf": [
1408
+ { "type": "string", "minLength": 1 },
1409
+ {
1410
+ "type": "object",
1411
+ "additionalProperties": false,
1412
+ "required": ["key"],
1413
+ "properties": {
1414
+ "key": { "type": "string", "minLength": 1 },
1415
+ "label": { "type": "string" },
1416
+ "color": { "type": "string" },
1417
+ "hidden": { "type": "boolean" },
1418
+ "closed": { "type": "boolean" }
1419
+ }
1420
+ }
1421
+ ]
1398
1422
  }
1399
1423
  },
1400
1424
  "headerPill": {