@conduction/nextcloud-vue 2.92.0 → 2.94.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 (199) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  17. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  18. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
  21. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
  23. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +111 -42
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +117 -73
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -39
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
  32. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  41. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  43. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  45. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  46. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  50. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  51. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  52. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  53. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  54. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +21 -0
  57. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  60. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  61. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
  65. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  67. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  68. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  70. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  72. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  73. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  74. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  75. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  76. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
  77. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
  78. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
  79. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
  80. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
  81. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
  82. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
  83. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
  84. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
  85. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
  86. package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
  87. package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
  88. package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -3
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  94. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  95. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  96. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  98. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
  99. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  100. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
  101. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  102. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  103. package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
  104. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  105. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  106. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  107. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
  108. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
  110. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  112. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  113. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  114. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  115. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  116. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  117. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  118. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  119. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
  120. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  121. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  122. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +32 -0
  123. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  124. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
  125. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  126. package/dist/esm/composables/indexSources.js +9 -0
  127. package/dist/esm/composables/indexSources.js.map +1 -1
  128. package/dist/esm/l10n/en.json.js +24 -1
  129. package/dist/esm/l10n/en.json.js.map +1 -1
  130. package/dist/esm/l10n/nl.json.js +24 -1
  131. package/dist/esm/l10n/nl.json.js.map +1 -1
  132. package/dist/esm/library-style.js +1 -1
  133. package/dist/esm/nextcloud-vue.css +1190 -410
  134. package/dist/esm/utils/dueText.js +49 -8
  135. package/dist/esm/utils/dueText.js.map +1 -1
  136. package/dist/esm/utils/featureCatalog.js +135 -0
  137. package/dist/esm/utils/featureCatalog.js.map +1 -0
  138. package/dist/esm/utils/fillTemplate.js +74 -3
  139. package/dist/esm/utils/fillTemplate.js.map +1 -1
  140. package/dist/esm/utils/headerMeta.js +62 -12
  141. package/dist/esm/utils/headerMeta.js.map +1 -1
  142. package/dist/esm/utils/roadmapIssues.js +86 -0
  143. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  144. package/dist/esm/utils/userDisplayName.js +47 -0
  145. package/dist/esm/utils/userDisplayName.js.map +1 -0
  146. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  147. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  148. package/dist/nextcloud-vue.cjs.js +16229 -13423
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +1190 -410
  151. package/l10n/en.json +24 -1
  152. package/l10n/nl.json +24 -1
  153. package/package.json +1 -1
  154. package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
  155. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
  156. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  157. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  158. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  159. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  160. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
  161. package/src/components/CnDashboardPage/CnDashboardPage.vue +141 -8
  162. package/src/components/CnDataTable/CnDataTable.md +1 -0
  163. package/src/components/CnDataTable/CnDataTable.vue +41 -46
  164. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  165. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  166. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  167. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  168. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  169. package/src/components/CnFormDialog/CnFormDialog.vue +606 -562
  170. package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
  171. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  172. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  173. package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
  174. package/src/components/CnPageHeader/CnPageHeader.md +11 -0
  175. package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
  176. package/src/components/CnPagination/CnPagination.vue +10 -2
  177. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +3 -3
  178. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  179. package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
  180. package/src/components/CnTabs/CnTabs.vue +39 -2
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
  182. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  183. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  184. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
  185. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +43 -2
  186. package/src/composables/indexSources.js +9 -0
  187. package/src/css/dialog.css +61 -0
  188. package/src/css/look-board-detail.css +8 -0
  189. package/src/css/look-board-index.css +27 -1
  190. package/src/css/look-board.css +60 -0
  191. package/src/css/page-header.css +16 -0
  192. package/src/schemas/app-manifest-v2.schema.json +30 -17
  193. package/src/utils/dueText.js +49 -8
  194. package/src/utils/featureCatalog.js +132 -0
  195. package/src/utils/fillTemplate.js +73 -2
  196. package/src/utils/headerMeta.js +62 -12
  197. package/src/utils/roadmapIssues.js +82 -0
  198. package/src/utils/userDisplayName.js +54 -0
  199. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -47,6 +47,8 @@
47
47
  :title="title"
48
48
  :description="resolvedDescription"
49
49
  :icon="icon"
50
+ :breadcrumb="breadcrumb"
51
+ :meta="headerMeta"
50
52
  :visuallyHidden="!showTitle">
51
53
  <!-- Board look: the buildiq square, then the page Save as the
52
54
  last header button. -->
@@ -475,6 +477,30 @@ export default {
475
477
  default: () => t('nextcloud-vue', 'Settings'),
476
478
  },
477
479
 
480
+ /**
481
+ * A breadcrumb under the title (manifest `config.breadcrumb`): one
482
+ * crumb `{ label, route?, params?, href?, icon?, separator? }` or a
483
+ * list; the title follows as the current crumb. Drawn as the detail
484
+ * header's breadcrumb row. Null (the default) draws none.
485
+ *
486
+ * @type {object|Array<object>|null}
487
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
488
+ */
489
+ breadcrumb: {
490
+ type: [Object, Array],
491
+ default: null,
492
+ },
493
+
494
+ /**
495
+ * A note after the breadcrumb and a middle dot (manifest `config.headerMeta`).
496
+ *
497
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
498
+ */
499
+ headerMeta: {
500
+ type: String,
501
+ default: '',
502
+ },
503
+
478
504
  /** Description shown under the title when `showTitle` is set. */
479
505
  description: {
480
506
  type: String,
@@ -263,6 +263,21 @@ export default defineComponent({
263
263
  default: '',
264
264
  },
265
265
 
266
+ /**
267
+ * Leave the `overflow` tabs to the host's `#nav-end` menu instead of a
268
+ * "More" menu of the strip's own. The strip then draws only the tabs
269
+ * that are not `overflow`, and an overflow tab that is selected joins
270
+ * it just before the last of them (board DqZaakBesluiten: Overview,
271
+ * Files, Contact, Tasks, Decisions, History). The host lists the
272
+ * overflow tabs and selects one through its `CnTab`'s `active`.
273
+ *
274
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-board-tabs-list-their-overflow-in-the-actions-menu
275
+ */
276
+ overflowInNavEnd: {
277
+ type: Boolean,
278
+ default: false,
279
+ },
280
+
266
281
  /** Name of the menu that lists the `overflow` tabs. */
267
282
  moreLabel: {
268
283
  type: String,
@@ -377,14 +392,36 @@ export default defineComponent({
377
392
  *
378
393
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
379
394
  */
380
- const visibleTabs = computed(() => tabs.filter((tab) => boardStrip.value || !tab.overflow || tab.uid === activeUid.value))
395
+ const visibleTabs = computed(() => {
396
+ if (props.overflowInNavEnd) {
397
+ return navEndStrip()
398
+ }
399
+ return tabs.filter((tab) => boardStrip.value || !tab.overflow || tab.uid === activeUid.value)
400
+ })
401
+
402
+ /**
403
+ * The strip under `overflowInNavEnd`: the tabs that are not
404
+ * `overflow`, with a selected overflow tab placed before the last.
405
+ *
406
+ * @return {object[]} The tabs to draw, in order.
407
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-an-opened-overflow-tab-joins-the-strip-before-the-last-tab
408
+ */
409
+ function navEndStrip() {
410
+ const pinned = tabs.filter((tab) => !tab.overflow)
411
+ const opened = tabs.find((tab) => tab.overflow && tab.uid === activeUid.value)
412
+ if (!opened) {
413
+ return pinned
414
+ }
415
+ const at = Math.max(0, pinned.length - 1)
416
+ return [...pinned.slice(0, at), opened, ...pinned.slice(at)]
417
+ }
381
418
 
382
419
  /**
383
420
  * The tabs listed under "More": overflow tabs that are not selected.
384
421
  *
385
422
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
386
423
  */
387
- const overflowTabs = computed(() => (boardStrip.value ? [] : tabs.filter((tab) => tab.overflow && tab.uid !== activeUid.value)))
424
+ const overflowTabs = computed(() => (boardStrip.value || props.overflowInNavEnd ? [] : tabs.filter((tab) => tab.overflow && tab.uid !== activeUid.value)))
388
425
 
389
426
  /**
390
427
  * Whether a tab declares a count. `0` is a count; null and '' are not.
@@ -9,6 +9,7 @@
9
9
  :aria-label="stripLabel"
10
10
  :moreLabel="moreLabel"
11
11
  :variant="stripVariant"
12
+ :overflowInNavEnd="isBoard"
12
13
  class="cn-tabs-widget__tabs"
13
14
  @update:activeIndex="onTabChange">
14
15
  <!-- One Actions menu for the whole widget, bound to whichever child
@@ -27,10 +28,11 @@
27
28
  {{ cnTranslate(activeHeaderLink.label) }}
28
29
  </component>
29
30
  <CnActionsMenu
30
- v-if="showActions"
31
- :showRefresh="showRefresh"
32
- :showRequestFeature="showRequestFeature"
33
- :showDocumentation="showDocumentation"
31
+ v-if="showActions || menuTabs.length"
32
+ :showRefresh="showActions && showRefresh"
33
+ :showRequestFeature="showActions && showRequestFeature"
34
+ :showDocumentation="showActions && showDocumentation"
35
+ :showReportBug="showActions"
34
36
  :documentationUrl="documentationUrl"
35
37
  :widgetId="activeWidgetId"
36
38
  :title="activeTitle"
@@ -38,6 +40,20 @@
38
40
  :surface="`widget:${activeWidgetId}`"
39
41
  refreshChannel="cn:widget:refresh"
40
42
  testidBase="cn-tabs-widget">
43
+ <!-- Board look: the tabs that are not in the strip, at the
44
+ top of the menu, in place of a "More" button. -->
45
+ <template v-if="menuTabs.length" #lead-items>
46
+ <NcActionButton
47
+ v-for="entry in menuTabs"
48
+ :key="entry.key"
49
+ :disabled="entry.pending || null"
50
+ :closeAfterClick="true"
51
+ data-testid="cn-tabs-widget-menu-tab"
52
+ @click="openMenuTab(entry)">
53
+ {{ menuTabLabel(entry) }}
54
+ </NcActionButton>
55
+ <NcActionSeparator v-if="showActions" />
56
+ </template>
41
57
  <!-- The open panel's own items, below the built-in trio.
42
58
  A panel draws no header, so these would otherwise have
43
59
  nowhere to go: the data widget's Metadata and full edit
@@ -51,7 +67,7 @@
51
67
  tab mounted, so several panels publish at once and an
52
68
  unfiltered list would offer actions for a sheet nobody
53
69
  is looking at. -->
54
- <template v-if="activePanelActions.length" #action-items>
70
+ <template v-if="showActions && activePanelActions.length" #action-items>
55
71
  <NcActionButton
56
72
  v-for="action in activePanelActions"
57
73
  :key="action.key"
@@ -175,7 +191,7 @@
175
191
  <script>
176
192
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
177
193
  import { translate as t } from '@nextcloud/l10n'
178
- import { NcActionButton, NcLoadingIcon } from '@nextcloud/vue'
194
+ import { NcActionButton, NcActionSeparator, NcLoadingIcon } from '@nextcloud/vue'
179
195
  import CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'
180
196
  import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
181
197
  import CnIcon from '../CnIcon/CnIcon.vue'
@@ -184,6 +200,7 @@ import CnTabs from '../CnTabs/CnTabs.vue'
184
200
  import { normalizeLook } from '../../composables/useLook.js'
185
201
  import { headerLinkTarget, widgetHeaderLink } from '../../utils/contentHeaderLink.js'
186
202
  import { resolveTabCount } from '../../utils/detailActionModel.js'
203
+ import { detailWidgetVisible } from '../../utils/detailWidgetVisibility.js'
187
204
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
188
205
  import { isActivityWidget } from '../../utils/headerMeta.js'
189
206
  import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
@@ -251,6 +268,7 @@ export default {
251
268
  CnTab,
252
269
  CnTabs,
253
270
  NcActionButton,
271
+ NcActionSeparator,
254
272
  CnEmptyContent,
255
273
  NcLoadingIcon,
256
274
  },
@@ -364,7 +382,8 @@ export default {
364
382
  * `layout: [{ widgetId, gridWidth }]` sets a width (of 12) per widget, as
365
383
  * the page grid does. Widgets wrap onto the next row.
366
384
  *
367
- * `maxVisibleTabs` caps the strip: later tabs go under "More".
385
+ * `maxVisibleTabs` caps the strip: later tabs go under "More" (in the
386
+ * board look: into the Actions menu, with History kept in the strip).
368
387
  * `hideEmpty: true` moves a tab whose count is 0 there too.
369
388
  * `moreLabel` names that menu. `variant: "segmented"` draws the strip
370
389
  * as a pill switch (CnTabs' segmented variant); `line` is the default.
@@ -552,16 +571,34 @@ export default {
552
571
  return [...(forId.host || []), ...(forId.widget || [])]
553
572
  },
554
573
 
574
+ /**
575
+ * The tabs the board look lists at the top of the Actions menu: the
576
+ * overflow tabs, less the one that is open (it is in the strip).
577
+ * Empty outside the board look, where CnTabs draws "More".
578
+ *
579
+ * @return {object[]} Resolved tabs, in strip order.
580
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-board-tabs-list-their-overflow-in-the-actions-menu
581
+ */
582
+ menuTabs() {
583
+ if (!this.isBoard) {
584
+ return []
585
+ }
586
+ return this.resolvedTabs.filter((entry, index) => entry.overflow && index !== this.activeIndex)
587
+ },
588
+
555
589
  /**
556
590
  * The configured tabs, each paired with the widget definition it names.
557
591
  *
558
592
  * A tab goes under the strip's "More" menu when it says `overflow`, when
559
593
  * `content.hideEmpty` is set and its count is 0, or when
560
594
  * `content.maxVisibleTabs` places are already taken. `count` is a
561
- * literal, or read off the record through `countField`.
595
+ * literal, or read off the record through `countField`. In the board
596
+ * look those tabs are listed in the Actions menu instead, and the
597
+ * activity (History) tab always stays in the strip.
562
598
  *
563
599
  * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.
564
600
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
601
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-board-tabs-list-their-overflow-in-the-actions-menu
565
602
  */
566
603
  resolvedTabs() {
567
604
  const authored = Array.isArray(this.content?.tabs) ? this.content.tabs : []
@@ -580,9 +617,12 @@ export default {
580
617
  const originalIndex = ordered.map(({ index }) => index)
581
618
  const max = Number(this.content?.maxVisibleTabs)
582
619
  const hideEmpty = this.content?.hideEmpty === true
620
+ // The board keeps its History tab in the strip, so it holds one of
621
+ // the `maxVisibleTabs` places from the start.
622
+ const pinActivity = this.isBoard && tabs.some((tab, position) => isActivityTab(tab) && this.tabVisibility(tab, originalIndex[position]) !== 'hidden')
583
623
  // How many tabs have taken a place in the strip so far. A tab that
584
624
  // is already going under "More" does not use one up.
585
- let placed = 0
625
+ let placed = pinActivity ? 1 : 0
586
626
  const resolved = []
587
627
  tabs.forEach((tab, position) => {
588
628
  const index = originalIndex[position]
@@ -596,7 +636,7 @@ export default {
596
636
  const ids = this.tabWidgetIds(tab)
597
637
  const widgets = ids.length > 1
598
638
  ? ids.map((id) => this.availableWidgets.find((w) => w && w.id === id) || null)
599
- .filter((w) => w && !this.hiddenByWorkspace(w))
639
+ .filter((w) => w && !this.hiddenByWorkspace(w) && !this.hiddenByRecord(w))
600
640
  : []
601
641
  const widget = ids.length > 1
602
642
  ? (widgets[0] || null)
@@ -605,9 +645,9 @@ export default {
605
645
  const declared = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)
606
646
  const counted = this.widgetCounts[index]
607
647
  const count = declared === null && typeof counted === 'number' ? counted : declared
608
- // The board strip has no "More": every tab is in the strip.
609
- let overflow = !this.isBoard && ((tab && tab.overflow === true) || (hideEmpty && count === 0))
610
- if (!overflow && !this.isBoard) {
648
+ const pinned = pinActivity && isActivityTab(tab)
649
+ let overflow = !pinned && ((tab && tab.overflow === true) || (hideEmpty && count === 0))
650
+ if (!overflow && !pinned) {
611
651
  if (Number.isFinite(max) && max > 0 && placed >= max) {
612
652
  overflow = true
613
653
  } else {
@@ -868,6 +908,26 @@ export default {
868
908
  return !evaluateWorkspaceCondition(cond, 'value' in bag ? bag.value : bag)
869
909
  },
870
910
 
911
+ /**
912
+ * Whether the record hides a widget of a stack on a detail page: its
913
+ * own `visibleWhen` reads a field of the record or of a record the
914
+ * page's `visibilityContext` names (`@context.<name>`), and does not
915
+ * hold, as a grid widget's would not. Hidden while those records
916
+ * load. Off a detail page (no record), and for a condition that needs
917
+ * a request, nothing is hidden here.
918
+ *
919
+ * @param {object} widget The widget definition.
920
+ * @return {boolean} True when the widget must not draw.
921
+ * @spec openspec/changes/tab-stack-child-visible-when/specs/tab-stack-child-visible-when/spec.md#requirement-a-widget-in-a-tab-stack-on-a-detail-page-follows-its-own-condition
922
+ */
923
+ hiddenByRecord(widget) {
924
+ const cond = widget.visibleWhen
925
+ if (!this.objectData || !cond || typeof cond !== 'object' || isWorkspaceCondition(cond) || !isLocallyDecidableVisibleWhen(cond)) {
926
+ return false
927
+ }
928
+ return !detailWidgetVisible(widget, this.conditionData)
929
+ },
930
+
871
931
  /**
872
932
  * The grid a stack lays its widgets out in, as one column span (of 12)
873
933
  * per widget in order, or null for the plain vertical stack. A tab says
@@ -1139,6 +1199,39 @@ export default {
1139
1199
  }
1140
1200
  },
1141
1201
 
1202
+ /**
1203
+ * Open a tab picked from the Actions menu, then move focus to it in
1204
+ * the strip, so a keyboard user lands on the tab they chose.
1205
+ *
1206
+ * @param {object} entry The resolved tab.
1207
+ * @return {void}
1208
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-board-tabs-list-their-overflow-in-the-actions-menu
1209
+ */
1210
+ openMenuTab(entry) {
1211
+ const index = this.resolvedTabs.indexOf(entry)
1212
+ if (index < 0) {
1213
+ return
1214
+ }
1215
+ this.activeIndex = index
1216
+ this.$nextTick(() => {
1217
+ const button = this.$el?.querySelector?.('[role="tab"][aria-selected="true"]')
1218
+ if (button && typeof button.focus === 'function') {
1219
+ button.focus()
1220
+ }
1221
+ })
1222
+ },
1223
+
1224
+ /**
1225
+ * A menu tab's text: its label, and its count when it has one.
1226
+ *
1227
+ * @param {object} entry The resolved tab.
1228
+ * @return {string} The menu label.
1229
+ */
1230
+ menuTabLabel(entry) {
1231
+ const hasCount = entry.count !== null && entry.count !== undefined && entry.count !== ''
1232
+ return hasCount ? `${entry.label} (${entry.count})` : entry.label
1233
+ },
1234
+
1142
1235
  /**
1143
1236
  * Empty-state text for a tab whose widget id resolves to nothing.
1144
1237
  *
@@ -0,0 +1,305 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <CnDialog
7
+ class="cn-user-settings-dialog"
8
+ :name="title"
9
+ look="board"
10
+ width="wizard"
11
+ :eyebrow="eyebrow"
12
+ :subtitle="subtitle"
13
+ :open="open"
14
+ :style="eyebrowStyle"
15
+ data-testid="cn-user-settings-dialog"
16
+ @update:open="onUpdateOpen"
17
+ @closing="close">
18
+ <div class="cn-user-settings-dialog__body">
19
+ <nav
20
+ v-if="sections.length > 0"
21
+ class="cn-user-settings-dialog__nav"
22
+ :aria-label="navLabel"
23
+ data-testid="cn-user-settings-nav">
24
+ <ul>
25
+ <li v-for="section in sections" :key="section.id">
26
+ <a
27
+ :href="`#settings-section_${section.id}`"
28
+ class="cn-user-settings-dialog__nav-link"
29
+ :class="{ 'cn-user-settings-dialog__nav-link--active': section.id === selected }"
30
+ :aria-current="section.id === selected ? 'true' : undefined"
31
+ :data-testid="`cn-user-settings-nav-${section.id}`"
32
+ @click.prevent="selectSection(section.id)">
33
+ <span v-if="section.icon" class="cn-user-settings-dialog__nav-icon" aria-hidden="true">
34
+ <CnVNodes :vnodes="section.icon" />
35
+ </span>
36
+ <span>{{ section.name }}</span>
37
+ </a>
38
+ </li>
39
+ </ul>
40
+ </nav>
41
+ <div class="cn-user-settings-dialog__sections">
42
+ <!-- @slot default NcAppSettingsSection children; each registers itself in the navigation. -->
43
+ <slot />
44
+ </div>
45
+ </div>
46
+ <template #actions>
47
+ <p class="cn-user-settings-dialog__note" data-testid="cn-user-settings-note">
48
+ {{ footerNote }}
49
+ </p>
50
+ <NcButton variant="secondary" data-testid="cn-user-settings-close" @click="close">
51
+ {{ closeLabel }}
52
+ </NcButton>
53
+ </template>
54
+ </CnDialog>
55
+ </template>
56
+
57
+ <script>
58
+ import { translate as t } from '@nextcloud/l10n'
59
+ import { NcButton } from '@nextcloud/vue'
60
+ import { markRaw } from 'vue'
61
+ import CnDialog from '../CnDialog/CnDialog.vue'
62
+
63
+ /**
64
+ * The key NcAppSettingsSection injects to register itself with its dialog
65
+ * (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).
66
+ */
67
+ const REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration')
68
+
69
+ /** Renders a section's icon vnodes as they were registered. */
70
+ const CnVNodes = {
71
+ name: 'CnVNodes',
72
+ props: { vnodes: { type: [Array, Object], default: null } },
73
+ render() {
74
+ return this.vnodes
75
+ },
76
+ }
77
+
78
+ /**
79
+ * CnUserSettingsDialog - the user settings dialog in the board look (board
80
+ * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the
81
+ * sections with their icons, the sections themselves, and a footer with a
82
+ * note that changes save straight away and a Close button.
83
+ *
84
+ * The sections stay the app's: any `NcAppSettingsSection` in the default slot
85
+ * registers itself with this dialog exactly as it does with
86
+ * `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot
87
+ * renders this dialog instead of `NcAppSettingsDialog` under the board look.
88
+ *
89
+ * @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look
90
+ *
91
+ * @event update:open Emitted with `false` when the dialog closes.
92
+ */
93
+ export default {
94
+ name: 'CnUserSettingsDialog',
95
+
96
+ components: { CnDialog, CnVNodes, NcButton },
97
+
98
+ provide() {
99
+ return {
100
+ [REGISTRATION_KEY]: {
101
+ registerSection: this.registerSection,
102
+ unregisterSection: this.unregisterSection,
103
+ },
104
+ }
105
+ },
106
+
107
+ props: {
108
+ /** Whether the dialog is open. */
109
+ open: { type: Boolean, default: false },
110
+ /** Dialog title; also its accessible name. */
111
+ title: { type: String, required: true },
112
+ /** Context line above the title. */
113
+ eyebrow: { type: String, default: () => t('nextcloud-vue', 'Your account') },
114
+ /** Sentence under the title. */
115
+ subtitle: { type: String, default: () => t('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },
116
+ /** The app's name, after the footer note. */
117
+ appName: { type: String, default: '' },
118
+ },
119
+
120
+ emits: ['update:open'],
121
+
122
+ data() {
123
+ return {
124
+ sections: [],
125
+ selected: '',
126
+ picked: false,
127
+ }
128
+ },
129
+
130
+ computed: {
131
+ /** @return {object} The board's eyebrow: muted, in sentence case. */
132
+ eyebrowStyle() {
133
+ return { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }
134
+ },
135
+
136
+ /** @return {string} The navigation's accessible name. */
137
+ navLabel() {
138
+ return t('nextcloud-vue', 'Sections of the settings')
139
+ },
140
+
141
+ /** @return {string} The footer note, with the app's name when known. */
142
+ footerNote() {
143
+ const note = t('nextcloud-vue', 'Changes are saved straight away')
144
+ return this.appName ? `${note} · ${this.appName}` : note
145
+ },
146
+
147
+ /** @return {string} The Close button label. */
148
+ closeLabel() {
149
+ return t('nextcloud-vue', 'Close')
150
+ },
151
+ },
152
+
153
+ methods: {
154
+ /**
155
+ * Add a section to the navigation, kept in `order`; a section
156
+ * without one goes last. Until the reader picks one, the first
157
+ * section is current.
158
+ *
159
+ * @param {string} id The section id.
160
+ * @param {string} name The section name.
161
+ * @param {number} [order] The section's place.
162
+ * @param {Array} [icon] The section's icon vnodes.
163
+ */
164
+ registerSection(id, name, order, icon) {
165
+ const rest = this.sections.filter((section) => section.id !== id)
166
+ const place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1
167
+ this.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order)
168
+ if (!this.picked) {
169
+ this.selected = this.sections[0].id
170
+ }
171
+ },
172
+
173
+ /**
174
+ * Remove a section from the navigation.
175
+ *
176
+ * @param {string} id The section id.
177
+ */
178
+ unregisterSection(id) {
179
+ this.sections = this.sections.filter((section) => section.id !== id)
180
+ if (this.selected === id) {
181
+ this.selected = this.sections.length > 0 ? this.sections[0].id : ''
182
+ }
183
+ },
184
+
185
+ /**
186
+ * Mark a section current and scroll it into view.
187
+ *
188
+ * @param {string} id The section id.
189
+ */
190
+ selectSection(id) {
191
+ this.picked = true
192
+ this.selected = id
193
+ const target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null
194
+ if (target && typeof target.scrollIntoView === 'function') {
195
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Follow NcDialog's own open state.
201
+ *
202
+ * @param {boolean} value Whether NcDialog is open.
203
+ */
204
+ onUpdateOpen(value) {
205
+ if (!value) {
206
+ this.close()
207
+ }
208
+ },
209
+
210
+ /** Close the dialog. */
211
+ close() {
212
+ /**
213
+ * @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).
214
+ */
215
+ this.$emit('update:open', false)
216
+ },
217
+ },
218
+ }
219
+ </script>
220
+
221
+ <style scoped>
222
+ .cn-user-settings-dialog__body {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 0 24px;
226
+ }
227
+
228
+ .cn-user-settings-dialog__nav {
229
+ flex: 0 0 200px;
230
+ width: 200px;
231
+ padding-top: 18px;
232
+ }
233
+
234
+ .cn-user-settings-dialog__nav ul {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 2px;
238
+ margin: 0;
239
+ padding: 0;
240
+ list-style: none;
241
+ }
242
+
243
+ .cn-user-settings-dialog__nav-link {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 10px;
247
+ min-height: 40px;
248
+ padding: 0 12px;
249
+ border-radius: var(--border-radius-element, 8px);
250
+ font-size: 15px;
251
+ color: var(--color-main-text);
252
+ text-decoration: none;
253
+ }
254
+
255
+ .cn-user-settings-dialog__nav-link:hover {
256
+ background: var(--color-background-hover);
257
+ }
258
+
259
+ .cn-user-settings-dialog__nav-link:focus-visible {
260
+ outline: 2px solid var(--color-primary-element);
261
+ outline-offset: 2px;
262
+ }
263
+
264
+ .cn-user-settings-dialog__nav-link--active,
265
+ .cn-user-settings-dialog__nav-link--active:hover {
266
+ background: var(--color-primary-element-light);
267
+ color: var(--color-primary-element-light-text);
268
+ font-weight: 600;
269
+ }
270
+
271
+ .cn-user-settings-dialog__nav-icon {
272
+ display: inline-flex;
273
+ flex: none;
274
+ width: 18px;
275
+ height: 18px;
276
+ align-items: center;
277
+ justify-content: center;
278
+ }
279
+
280
+ .cn-user-settings-dialog__sections {
281
+ display: flex;
282
+ flex: 1 1 380px;
283
+ flex-direction: column;
284
+ min-width: 0;
285
+ }
286
+
287
+ .cn-user-settings-dialog__sections :deep(.app-settings-section) {
288
+ margin: 0;
289
+ padding: 18px 0;
290
+ border-top: 1px solid var(--color-border);
291
+ }
292
+
293
+ .cn-user-settings-dialog__sections :deep(.app-settings-section__name) {
294
+ margin: 0 0 12px;
295
+ font-size: 17px;
296
+ font-weight: 700;
297
+ }
298
+
299
+ .cn-user-settings-dialog__note {
300
+ flex: 1 1 auto;
301
+ margin: 0;
302
+ font-size: 14px;
303
+ color: var(--color-text-maxcontrast);
304
+ }
305
+ </style>
@@ -45,10 +45,11 @@
45
45
  <span class="cn-version-info__value">{{ appVersion }}</span>
46
46
  </div>
47
47
 
48
- <!-- Configured Version (if provided) -->
49
- <div v-if="configuredVersion" class="cn-version-info__item">
48
+ <!-- Configured version: when provided, and always in the board look,
49
+ which draws the row ("Unknown" until the app reports it). -->
50
+ <div v-if="configuredVersion || isBoard" class="cn-version-info__item" data-testid="cn-version-info-configured">
50
51
  <span class="cn-version-info__label">{{ fieldLabels.configuredVersion }}:</span>
51
- <span class="cn-version-info__value">{{ configuredVersion }}</span>
52
+ <span class="cn-version-info__value">{{ configuredVersion || t('nextcloud-vue', 'Unknown') }}</span>
52
53
  </div>
53
54
 
54
55
  <!-- Additional items -->
@@ -177,3 +177,11 @@ Request a feature / Report a bug / Documentation render on every widget; the sha
177
177
  ### A text link in the header (`headerLink`)
178
178
 
179
179
  `headerLink` (`{ label, route?, params?, query?, href? }`) draws one text link in the header before the overflow menu, such as "All deadlines".
180
+
181
+ Subtitle: `subtitle` is a muted line under the title, the counterpart of `eyebrow` above it:
182
+
183
+ ```vue
184
+ <CnWidgetWrapper title="Op tijd beslist per zaaktype" subtitle="Dit kwartaal, beslist binnen de termijn">
185
+ <p>…</p>
186
+ </CnWidgetWrapper>
187
+ ```