@conduction/nextcloud-vue 2.93.0 → 2.95.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 (162) 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 +46 -28
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  17. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
  18. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +91 -36
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +115 -72
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +32 -3
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
  34. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
  36. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
  38. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
  40. package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
  47. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
  48. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
  49. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
  55. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
  56. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
  57. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
  58. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
  59. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
  60. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
  61. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
  62. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
  63. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
  64. package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
  65. package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
  66. package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +51 -4
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  73. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +230 -25
  74. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
  76. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  77. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  78. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
  79. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  80. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
  81. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  82. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  83. package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
  84. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  86. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  87. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
  88. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
  90. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  91. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  92. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  93. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
  94. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  95. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  96. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  97. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  98. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +20 -0
  99. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  100. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
  101. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  102. package/dist/esm/composables/indexSources.js +9 -0
  103. package/dist/esm/composables/indexSources.js.map +1 -1
  104. package/dist/esm/index.js +1 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +18 -1
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +18 -1
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/library-style.js +1 -1
  111. package/dist/esm/nextcloud-vue.css +712 -330
  112. package/dist/esm/utils/dueText.js +49 -8
  113. package/dist/esm/utils/dueText.js.map +1 -1
  114. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  115. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  116. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  117. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  118. package/dist/nextcloud-vue.cjs.js +8275 -6302
  119. package/dist/nextcloud-vue.cjs.js.map +2 -2
  120. package/dist/nextcloud-vue.css +712 -330
  121. package/l10n/en.json +18 -1
  122. package/l10n/nl.json +18 -1
  123. package/package.json +1 -1
  124. package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
  125. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
  126. package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
  127. package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
  128. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
  129. package/src/components/CnDashboardPage/CnDashboardPage.vue +96 -2
  130. package/src/components/CnDataTable/CnDataTable.md +1 -0
  131. package/src/components/CnDataTable/CnDataTable.vue +39 -10
  132. package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
  133. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
  134. package/src/components/CnDeviceFramesWidget/index.js +29 -0
  135. package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
  136. package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
  137. package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
  138. package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
  139. package/src/components/CnPageHeader/CnPageHeader.md +11 -0
  140. package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
  141. package/src/components/CnPagination/CnPagination.vue +10 -2
  142. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +84 -4
  143. package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
  144. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
  145. package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
  146. package/src/components/CnTabs/CnTabs.vue +39 -2
  147. package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
  148. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  149. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  150. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
  151. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +31 -2
  152. package/src/components/index.js +1 -0
  153. package/src/composables/indexSources.js +9 -0
  154. package/src/css/dialog.css +14 -0
  155. package/src/css/look-board-index.css +27 -1
  156. package/src/css/look-board.css +27 -0
  157. package/src/css/page-header.css +16 -0
  158. package/src/index.js +1 -0
  159. package/src/schemas/app-manifest-v2.schema.json +27 -15
  160. package/src/utils/dueText.js +49 -8
  161. package/src/utils/libraryWidgetKeys.js +1 -0
  162. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -21,6 +21,8 @@
21
21
  :title="title"
22
22
  :description="headerDescription"
23
23
  :icon="showTitleIcon && !isBoardLook ? resolvedIcon : ''"
24
+ :breadcrumb="breadcrumb"
25
+ :meta="headerMetaText"
24
26
  :visuallyHidden="!showTitle">
25
27
  <!-- The board's header buttons (`headerButtons`), beside the
26
28
  title. Declaring them takes the Views and Actions menus
@@ -734,6 +736,7 @@
734
736
  :columnOverrides="columnOverrides"
735
737
  :rowClass="rowClass"
736
738
  :rowTitle="rowTitle"
739
+ :showUnread="showUnreadRows"
737
740
  :filterable="tableHeaderFilters"
738
741
  :activeFilters="effectiveActiveFilters"
739
742
  :filterRegister="typeof register === 'string' ? register : ''"
@@ -1609,6 +1612,31 @@ export default {
1609
1612
  required: true,
1610
1613
  },
1611
1614
 
1615
+ /**
1616
+ * A breadcrumb under the title (manifest `config.breadcrumb`): one
1617
+ * crumb `{ label, route?, params?, href?, icon?, separator? }` or a
1618
+ * list; the title follows as the current crumb. Drawn as the detail
1619
+ * header's breadcrumb row. Null (the default) draws none.
1620
+ *
1621
+ * @type {object|Array<object>|null}
1622
+ * @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
1623
+ */
1624
+ breadcrumb: {
1625
+ type: [Object, Array],
1626
+ default: null,
1627
+ },
1628
+
1629
+ /**
1630
+ * A note after the breadcrumb and a middle dot (manifest `config.headerMeta`),
1631
+ * such as "{total} open": `{total}` and `{shown}` are the list's counts.
1632
+ *
1633
+ * @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
1634
+ */
1635
+ headerMeta: {
1636
+ type: String,
1637
+ default: '',
1638
+ },
1639
+
1612
1640
  /** Optional description shown below the title */
1613
1641
  description: {
1614
1642
  type: String,
@@ -2232,7 +2260,7 @@ export default {
2232
2260
  * can open is an ordinary segment; one it cannot (no board columns, no
2233
2261
  * map configured) is drawn disabled, named "<mode>: not available on
2234
2262
  * this list". Fed from `pages[].config.viewSwitch`. Ignored without the
2235
- * board look, and absent the switch is as before.
2263
+ * board look; absent, the board look draws all four.
2236
2264
  *
2237
2265
  * @type {Array<'table' | 'cards' | 'board' | 'map'>}
2238
2266
  * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
@@ -3555,6 +3583,8 @@ export default {
3555
3583
  data() {
3556
3584
  return {
3557
3585
  currentViewMode: this.viewMode,
3586
+ /** Whether `?_view=` names a mode the page did not offer yet, to open once it does. */
3587
+ routeViewPending: false,
3558
3588
  /** The column field the reader picked in the board's column switch; '' = the default. */
3559
3589
  boardColumnFieldChoice: '',
3560
3590
  /** The rows a reference column field is built from, by field. */
@@ -4118,11 +4148,14 @@ export default {
4118
4148
  * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
4119
4149
  */
4120
4150
  viewSwitchDisabledModes() {
4121
- if (!this.isBoardLook || !Array.isArray(this.viewSwitch)) {
4151
+ if (!this.isBoardLook) {
4122
4152
  return []
4123
4153
  }
4154
+ // The index boards draw all four segments, so the board look does
4155
+ // too unless the manifest names its own list.
4156
+ const listed = Array.isArray(this.viewSwitch) ? this.viewSwitch : ['table', 'cards', 'board', 'map']
4124
4157
  const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
4125
- return this.viewSwitch.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
4158
+ return listed.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
4126
4159
  },
4127
4160
 
4128
4161
  /**
@@ -4160,6 +4193,17 @@ export default {
4160
4193
  })
4161
4194
  },
4162
4195
 
4196
+ /**
4197
+ * The modes `?_view=` can open: the toggle's modes plus the map when
4198
+ * the page offers it.
4199
+ *
4200
+ * @return {Array<string>} The modes.
4201
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-the-address-opens-a-view-mode-that-arrives-later
4202
+ */
4203
+ routeViewModes() {
4204
+ return [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
4205
+ },
4206
+
4163
4207
  /**
4164
4208
  * The column fields the board offers (`board.columnFields`), for the
4165
4209
  * column switch (screens-board-reference-columns).
@@ -5380,6 +5424,21 @@ export default {
5380
5424
  return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
5381
5425
  },
5382
5426
 
5427
+ /**
5428
+ * Whether the table marks unread rows. The boards draw every row alike,
5429
+ * so under the board look only a page that offers the Unread lens
5430
+ * (`personalLenses`) marks them; the Nextcloud look always does.
5431
+ *
5432
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-unread-rows-are-marked-only-where-the-reader-asked-for-them
5433
+ * @return {boolean} True when unread rows read in bold with the dot.
5434
+ */
5435
+ showUnreadRows() {
5436
+ if (!this.isBoardLook) {
5437
+ return true
5438
+ }
5439
+ return Array.isArray(this.personalLenses) && this.personalLenses.includes('unread')
5440
+ },
5441
+
5383
5442
  /** Resolved schema OBJECT (for column generation / icons / labels). */
5384
5443
  /**
5385
5444
  * Whether the table shows its header filters. A self-fetching page
@@ -6002,8 +6061,9 @@ export default {
6002
6061
  }
6003
6062
  // The board's header: Export reads "Download" with its icon and
6004
6063
  // the Actions menu reads "Actions", without the manifest saying so.
6005
- if (this.isBoardLook && label === '' && button.action === 'export') {
6006
- label = t('nextcloud-vue', 'Download')
6064
+ // A labelled Export keeps the icon too, unless it names its own.
6065
+ if (this.isBoardLook && button.action === 'export') {
6066
+ label = label || t('nextcloud-vue', 'Download')
6007
6067
  icon = icon || 'Download'
6008
6068
  }
6009
6069
  if (label === '' && button.action === 'actions-menu') {
@@ -6286,6 +6346,22 @@ export default {
6286
6346
  * @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-the-count-reads-in-the-users-language
6287
6347
  * @return {string}
6288
6348
  */
6349
+ /**
6350
+ * The header meta note with the list's counts filled in.
6351
+ *
6352
+ * @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
6353
+ * @return {string}
6354
+ */
6355
+ headerMetaText() {
6356
+ if (!this.headerMeta) {
6357
+ return ''
6358
+ }
6359
+ const total = this.effectivePagination?.total
6360
+ return this.cnTranslate(this.headerMeta)
6361
+ .replace('{shown}', String(this.effectiveObjects.length))
6362
+ .replace('{total}', typeof total === 'number' ? String(total) : '')
6363
+ },
6364
+
6289
6365
  headerDescription() {
6290
6366
  const total = this.effectivePagination?.total
6291
6367
  // Board look: the count line is a template over the rows on this page
@@ -6476,6 +6552,12 @@ export default {
6476
6552
  this.applyViewModeFromRoute()
6477
6553
  },
6478
6554
 
6555
+ routeViewModes() {
6556
+ if (this.routeViewPending) {
6557
+ this.applyViewModeFromRoute()
6558
+ }
6559
+ },
6560
+
6479
6561
  boardFetchKey: {
6480
6562
  immediate: true,
6481
6563
  handler(key) {
@@ -6908,6 +6990,10 @@ export default {
6908
6990
  */
6909
6991
  async loadFilterCounts() {
6910
6992
  const requestKey = this.countRequestKey
6993
+ if (this.isNamedSource) {
6994
+ await this.loadNamedSourceCounts(requestKey)
6995
+ return
6996
+ }
6911
6997
  const schema = this.exportSchemaSlug
6912
6998
  if (requestKey === '' || !this.register || !schema) {
6913
6999
  this.tabCounts = null
@@ -6939,6 +7025,46 @@ export default {
6939
7025
  this.savedViewCounts = viewCounts
6940
7026
  },
6941
7027
 
7028
+ /**
7029
+ * The quick-filter counts of a named source (`entitySource`): each tab
7030
+ * that asks is counted by the source's own `count(config)`, with the
7031
+ * tab's filter over `sourceConfig`. A source without `count` shows
7032
+ * none; a failed count leaves that tab without a number.
7033
+ *
7034
+ * @param {string} requestKey The `countRequestKey` this load answers.
7035
+ * @return {Promise<void>}
7036
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-a-named-source-counts-its-quick-filters
7037
+ */
7038
+ async loadNamedSourceCounts(requestKey) {
7039
+ const count = this.namedSource ? this.namedSource.count : null
7040
+ if (requestKey === '' || typeof count !== 'function') {
7041
+ this.tabCounts = null
7042
+ this.savedViewCounts = null
7043
+ return
7044
+ }
7045
+ const params = (this.$route && this.$route.params) || {}
7046
+ const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
7047
+ const tabCounts = {}
7048
+ await Promise.all(this.countEntries
7049
+ .filter((entry) => entry.key.startsWith('tab:'))
7050
+ .map(async (entry) => {
7051
+ const config = resolveFilterMap({ ...(this.sourceConfig || {}), ...(entry.filter || {}) }, params, ctx)
7052
+ try {
7053
+ const total = await count(config)
7054
+ if (Number.isFinite(total)) {
7055
+ tabCounts[Number(entry.key.slice(4))] = total
7056
+ }
7057
+ } catch {
7058
+ // No number for this tab.
7059
+ }
7060
+ }))
7061
+ if (requestKey !== this.countRequestKey) {
7062
+ return
7063
+ }
7064
+ this.tabCounts = tabCounts
7065
+ this.savedViewCounts = null
7066
+ },
7067
+
6942
7068
  // ── Split view (case-page-and-list-as-a-place) ──────────────────
6943
7069
 
6944
7070
  /**
@@ -9203,6 +9329,7 @@ export default {
9203
9329
  * @param {string} mode 'table' or 'cards'
9204
9330
  */
9205
9331
  onViewModeChange(mode) {
9332
+ this.routeViewPending = false
9206
9333
  this.currentViewMode = mode
9207
9334
  this.$emit('view-mode-change', mode)
9208
9335
  },
@@ -9212,16 +9339,26 @@ export default {
9212
9339
  * offers it. The key starts with `_`, like `_search` and `_page`, so it
9213
9340
  * is never read as a list filter. A menu entry's `query` can so open a
9214
9341
  * list as its board, on the one page that lists the schema
9215
- * (screens-board-reference-columns).
9342
+ * (screens-board-reference-columns). A mode the page offers only later
9343
+ * (the map, once rows with a geometry are in) opens when it arrives,
9344
+ * unless the reader picked a view first.
9216
9345
  *
9217
9346
  * @return {void}
9218
9347
  * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-the-address-can-open-a-list-in-a-view-mode
9348
+ * @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-the-address-opens-a-view-mode-that-arrives-later
9219
9349
  */
9220
9350
  applyViewModeFromRoute() {
9221
9351
  const wanted = this.$route?.query?._view
9222
- if (typeof wanted === 'string' && wanted !== this.currentViewMode && this.effectiveToggleModes.includes(wanted)) {
9352
+ if (typeof wanted !== 'string' || wanted === '' || wanted === this.currentViewMode) {
9353
+ this.routeViewPending = false
9354
+ return
9355
+ }
9356
+ if (this.routeViewModes.includes(wanted)) {
9223
9357
  this.currentViewMode = wanted
9358
+ this.routeViewPending = false
9359
+ return
9224
9360
  }
9361
+ this.routeViewPending = true
9225
9362
  },
9226
9363
 
9227
9364
  /**
@@ -0,0 +1,140 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div
7
+ v-if="crumbs.length > 0 || meta"
8
+ class="cn-header-trail cn-detail-page__header-row2"
9
+ data-testid="cn-header-trail">
10
+ <CnBreadcrumbs
11
+ v-if="crumbs.length > 0"
12
+ class="cn-detail-page__breadcrumbs cn-detail-page__breadcrumbs--row2"
13
+ :crumbs="crumbs"
14
+ :rootText="crumbs[0].icon === undefined"
15
+ :separator="separator"
16
+ data-testid="cn-header-trail-breadcrumbs" />
17
+ <span
18
+ v-if="crumbs.length > 0 && meta"
19
+ class="cn-detail-page__header-dot"
20
+ aria-hidden="true">&middot;</span>
21
+ <span
22
+ v-if="meta"
23
+ class="cn-detail-page__header-meta"
24
+ data-testid="cn-header-trail-meta">{{ meta }}</span>
25
+ </div>
26
+ </template>
27
+
28
+ <script>
29
+ import { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'
30
+
31
+ /**
32
+ * The breadcrumb and meta line under a page title: "Instellingen / Zaaktypen
33
+ * · 12 zaaktypen". It draws the same markup and classes as the board look's
34
+ * detail header row, so a list, dashboard or settings page carries the
35
+ * detail page's breadcrumb style rather than one of its own.
36
+ *
37
+ * `breadcrumb` is one crumb or a list, each `{ label, route?, params?, href?,
38
+ * icon?, separator? }`. The page title follows as the current crumb.
39
+ */
40
+ export default {
41
+ name: 'CnHeaderTrail',
42
+
43
+ components: { CnBreadcrumbs },
44
+
45
+ inject: {
46
+ cnTranslate: { default: () => (key) => key },
47
+ },
48
+
49
+ props: {
50
+ /**
51
+ * The crumbs before the current page, one object or a list.
52
+ *
53
+ * @type {object|Array<object>|null}
54
+ * @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
55
+ */
56
+ breadcrumb: {
57
+ type: [Object, Array],
58
+ default: null,
59
+ },
60
+
61
+ /** The current crumb: the page title, already translated. */
62
+ current: {
63
+ type: String,
64
+ default: '',
65
+ },
66
+
67
+ /**
68
+ * The note after the crumbs and a middle dot, already resolved.
69
+ *
70
+ * @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
71
+ */
72
+ meta: {
73
+ type: String,
74
+ default: '',
75
+ },
76
+
77
+ /**
78
+ * Translate function for the crumb labels; falls back to the injected
79
+ * `cnTranslate`.
80
+ *
81
+ * @type {((key: string) => string)|null}
82
+ */
83
+ translate: {
84
+ type: Function,
85
+ default: null,
86
+ },
87
+ },
88
+
89
+ computed: {
90
+ /**
91
+ * The declared crumbs that carry a label.
92
+ *
93
+ * @return {Array<object>}
94
+ */
95
+ declared() {
96
+ const list = Array.isArray(this.breadcrumb) ? this.breadcrumb : (this.breadcrumb ? [this.breadcrumb] : [])
97
+ return list.filter((crumb) => crumb && typeof crumb.label === 'string' && crumb.label !== '')
98
+ },
99
+
100
+ /**
101
+ * The crumbs for CnBreadcrumbs: each declared crumb as a link, then the
102
+ * current page. Empty without a declared crumb.
103
+ *
104
+ * @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
105
+ * @return {Array<object>}
106
+ */
107
+ crumbs() {
108
+ if (this.declared.length === 0) {
109
+ return []
110
+ }
111
+ const out = this.declared.map((crumb) => {
112
+ const entry = { label: (this.translate ?? this.cnTranslate)(crumb.label) }
113
+ if (typeof crumb.icon === 'string' && crumb.icon !== '') {
114
+ entry.icon = crumb.icon
115
+ }
116
+ if (crumb.route) {
117
+ entry.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
118
+ } else if (crumb.href) {
119
+ entry.href = crumb.href
120
+ }
121
+ return entry
122
+ })
123
+ if (this.current) {
124
+ out.push({ label: this.current })
125
+ }
126
+ return out
127
+ },
128
+
129
+ /**
130
+ * The text between the crumbs: the first crumb's `separator`, else `/`.
131
+ *
132
+ * @return {string}
133
+ */
134
+ separator() {
135
+ const declared = this.declared[0] && this.declared[0].separator
136
+ return typeof declared === 'string' && declared !== '' ? declared : '/'
137
+ },
138
+ },
139
+ }
140
+ </script>
@@ -81,3 +81,14 @@ With extra content (e.g., a badge or status indicator):
81
81
  </template>
82
82
  </CnPageHeader>
83
83
  ```
84
+
85
+ Breadcrumb and meta: `breadcrumb` (one crumb `{ label, route?, params?, href?, icon?, separator? }` or a list) draws a
86
+ line under the title in the detail header's breadcrumb style, with the title as the current crumb; `meta` follows after a
87
+ middle dot:
88
+
89
+ ```vue
90
+ <CnPageHeader
91
+ title="Zaaktypen"
92
+ :breadcrumb="{ label: 'Instellingen', href: '#instellingen' }"
93
+ meta="14 zaaktypen" />
94
+ ```
@@ -13,6 +13,12 @@
13
13
  <h1 class="cn-page-header__title" data-testid="cn-page-title">
14
14
  {{ resolvedTitle }}
15
15
  </h1>
16
+ <CnHeaderTrail
17
+ v-if="!visuallyHidden"
18
+ :breadcrumb="breadcrumb"
19
+ :current="resolvedTitle"
20
+ :meta="resolvedMeta"
21
+ :translate="effectiveTranslate" />
16
22
  <p v-if="description && !visuallyHidden" class="cn-page-header__description" data-testid="cn-page-description">
17
23
  {{ resolvedDescription }}
18
24
  </p>
@@ -22,6 +28,7 @@
22
28
  </template>
23
29
 
24
30
  <script>
31
+ import CnHeaderTrail from './CnHeaderTrail.vue'
25
32
  import { CnIcon } from '../CnIcon/index.js'
26
33
 
27
34
  /**
@@ -64,6 +71,7 @@ export default {
64
71
  name: 'CnPageHeader',
65
72
 
66
73
  components: {
74
+ CnHeaderTrail,
67
75
  CnIcon,
68
76
  },
69
77
 
@@ -90,6 +98,31 @@ export default {
90
98
  default: '',
91
99
  },
92
100
 
101
+ /**
102
+ * A breadcrumb under the title: one crumb or a list, each
103
+ * `{ label, route?, params?, href?, icon?, separator? }`. The title
104
+ * follows as the current crumb. Drawn as the board look's detail
105
+ * header row.
106
+ *
107
+ * @type {object|Array<object>|null}
108
+ * @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
109
+ */
110
+ breadcrumb: {
111
+ type: [Object, Array],
112
+ default: null,
113
+ },
114
+
115
+ /**
116
+ * A note after the breadcrumb and a middle dot, such as "12 open". Run
117
+ * through the host translate function.
118
+ *
119
+ * @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
120
+ */
121
+ meta: {
122
+ type: String,
123
+ default: '',
124
+ },
125
+
93
126
  /** Optional MDI icon name (rendered via CnIcon) */
94
127
  icon: {
95
128
  type: String,
@@ -153,6 +186,15 @@ export default {
153
186
  resolvedDescription() {
154
187
  return this.description ? this.effectiveTranslate(this.description) : this.description
155
188
  },
189
+
190
+ /**
191
+ * The meta note run through the host translate function.
192
+ *
193
+ * @return {string}
194
+ */
195
+ resolvedMeta() {
196
+ return this.meta ? this.effectiveTranslate(this.meta) : ''
197
+ },
156
198
  },
157
199
  }
158
200
  </script>
@@ -8,13 +8,13 @@
8
8
  Board variant (`variant: "board"`): the footer of the table card as the
9
9
  screens draw it. The count text at the start, numbered 34px links at
10
10
  the end, "Previous" only after the first page and "Next" only before
11
- the last. No First, no Last, no page-size select.
11
+ the last (inert on a single page). No First, no Last, no page-size select.
12
12
  -->
13
13
  <template v-if="isBoardVariant">
14
14
  <span class="cn-pagination__page-info cn-pagination__board-count" data-testid="cn-pagination-board-count">
15
15
  {{ boardCountText }}
16
16
  </span>
17
- <nav v-if="totalPages > 1" class="cn-pagination__board-pages" :aria-label="t('nextcloud-vue', 'Pagination')">
17
+ <nav class="cn-pagination__board-pages" :aria-label="t('nextcloud-vue', 'Pagination')">
18
18
  <button
19
19
  v-if="currentPage > 1"
20
20
  type="button"
@@ -43,6 +43,14 @@
43
43
  @click="changePage(currentPage + 1)">
44
44
  {{ nextLabel }}
45
45
  </button>
46
+ <!-- A single page still draws its "1" and an inert Next, as the boards do. -->
47
+ <span
48
+ v-else-if="totalPages <= 1"
49
+ class="cn-pagination__board-link cn-pagination__board-link--wide cn-pagination__board-link--disabled"
50
+ aria-disabled="true"
51
+ data-testid="cn-pagination-next">
52
+ {{ nextLabel }}
53
+ </span>
46
54
  </nav>
47
55
  </template>
48
56
 
@@ -169,6 +169,20 @@
169
169
  {{ item.title }}
170
170
  </component>
171
171
  </div>
172
+ <!-- `bar`: a thin track under the row, filled to the row's value
173
+ against its own maximum or against the largest row. -->
174
+ <span
175
+ v-if="presentation === 'rows' && item.bar"
176
+ class="cn-record-list__bar"
177
+ :class="{ 'cn-record-list__bar--over': item.bar.over }"
178
+ role="meter"
179
+ :aria-label="item.title || null"
180
+ :aria-valuenow="item.bar.value"
181
+ aria-valuemin="0"
182
+ :aria-valuemax="item.bar.max"
183
+ data-testid="cn-record-list-bar">
184
+ <span class="cn-record-list__bar-fill" :style="{ width: `${item.bar.share}%` }" />
185
+ </span>
172
186
  </li>
173
187
  </ul>
174
188
  </div>
@@ -254,7 +268,11 @@ export default {
254
268
  * The list config: `{ register, schema, filter?, sort?, limit?, extend?,
255
269
  * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
256
270
  * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
257
- * valueLabels?, endpointSource? }`. `endpointSource` (`{ url, method?,
271
+ * valueLabels?, endpointSource?, bar? }`. `bar` (`{ valueField, maxField?,
272
+ * overVariant? }`) draws a thin track under each row: filled to
273
+ * `valueField` against `maxField` (drawn in the error colour past it
274
+ * with `overVariant: "error"`), or without `maxField` against the
275
+ * largest row. `endpointSource` (`{ url, method?,
258
276
  * params?, responsePath? }`) reads the rows from an app endpoint instead
259
277
  * of `register` / `schema`. `valueLabels` maps a field's stored values to the labels
260
278
  * the templates show.
@@ -402,6 +420,7 @@ export default {
402
420
  /** @return {Array<object>} The rows ready to draw. */
403
421
  items() {
404
422
  const c = this.content || {}
423
+ const barScale = this.barScale
405
424
  return this.rows.slice(0, this.limit).map((row, index) => {
406
425
  const link = this.rowLink(row)
407
426
  return {
@@ -420,10 +439,26 @@ export default {
420
439
  badges: this.badgesOf(row),
421
440
  due: this.dueOf(row),
422
441
  icon: this.iconOf(row),
442
+ bar: this.barOf(row, barScale),
423
443
  }
424
444
  })
425
445
  },
426
446
 
447
+ /**
448
+ * The largest `bar.valueField` over the rows shown, the scale of a
449
+ * `bar` without `maxField`. 0 without a `bar`.
450
+ *
451
+ * @return {number} The largest value.
452
+ * @spec openspec/changes/record-list-row-bar/specs/record-list-row-bar/spec.md#requirement-a-record-list-row-can-carry-a-bar
453
+ */
454
+ barScale() {
455
+ const field = this.content?.bar?.valueField
456
+ if (!field) {
457
+ return 0
458
+ }
459
+ return Math.max(0, ...this.rows.slice(0, this.limit).map((row) => Number(readPath(row, field)) || 0))
460
+ },
461
+
427
462
  /** @return {object|null} The header link with `{total}` filled and tokens resolved. */
428
463
  headerLink() {
429
464
  const link = this.content?.headerLink
@@ -518,6 +553,32 @@ export default {
518
553
  return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
519
554
  },
520
555
 
556
+ /**
557
+ * A row's `bar`: its value, the maximum it is measured against (the
558
+ * row's `maxField`, else the largest row), the filled share and
559
+ * whether it is over that maximum. Null without `bar` or a value.
560
+ *
561
+ * @param {object} row The row.
562
+ * @param {number} scale The largest value over the rows shown.
563
+ * @return {{value: number, max: number, share: number, over: boolean}|null} The bar.
564
+ * @spec openspec/changes/record-list-row-bar/specs/record-list-row-bar/spec.md#requirement-a-record-list-row-can-carry-a-bar
565
+ */
566
+ barOf(row, scale) {
567
+ const bar = this.content?.bar
568
+ if (!bar || typeof bar !== 'object' || !bar.valueField) {
569
+ return null
570
+ }
571
+ const value = Number(readPath(row, bar.valueField))
572
+ if (!Number.isFinite(value)) {
573
+ return null
574
+ }
575
+ const ownMax = bar.maxField ? Number(readPath(row, bar.maxField)) : NaN
576
+ const max = bar.maxField ? (Number.isFinite(ownMax) && ownMax > 0 ? ownMax : 0) : scale
577
+ const share = max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0
578
+ const over = Boolean(bar.maxField) && bar.overVariant === 'error' && max > 0 && value > max
579
+ return { value, max, share, over }
580
+ },
581
+
521
582
  /**
522
583
  * The initials of the row's avatar (`avatar`: a name template such as
523
584
  * `"{client.name}"`, or `{ text }`), or '' without one.
@@ -727,9 +788,9 @@ export default {
727
788
  },
728
789
 
729
790
  /**
730
- * The row's due text (`due`: `{ field, todayLabel?, overdueLabel?, emptyText? }`):
731
- * "3 dagen te laat" in the danger tone, "Vandaag", the date otherwise,
732
- * `emptyText` when the field is empty.
791
+ * The row's due text (`due`: `{ field, todayLabel?, overdueLabel?, upcomingLabel?, upcomingWeeksLabel?, upcomingWeeksFrom?, emptyText? }`):
792
+ * "3 dagen te laat" in the danger tone, "Vandaag", the date or the time
793
+ * left ("nog 4 dagen") otherwise, `emptyText` when the field is empty.
733
794
  *
734
795
  * @param {object} row The row.
735
796
  * @return {{text: string, tone: string}|null} The text and tone, or null.
@@ -1075,6 +1136,25 @@ export default {
1075
1136
  border-top: 1px solid var(--color-border);
1076
1137
  }
1077
1138
 
1139
+ .cn-record-list__bar {
1140
+ background: var(--color-border);
1141
+ border-radius: 3px;
1142
+ display: block;
1143
+ height: 6px;
1144
+ margin: -6px 0 12px;
1145
+ overflow: hidden;
1146
+ }
1147
+
1148
+ .cn-record-list__bar-fill {
1149
+ background: var(--color-primary-element);
1150
+ display: block;
1151
+ height: 100%;
1152
+ }
1153
+
1154
+ .cn-record-list__bar--over .cn-record-list__bar-fill {
1155
+ background: var(--color-error);
1156
+ }
1157
+
1078
1158
  .cn-record-list__row {
1079
1159
  align-items: center;
1080
1160
  color: var(--color-main-text);
@@ -115,6 +115,14 @@ export default {
115
115
 
116
116
  With more than one group each group starts closed: its heading is a button that shows the group name and how many of its types are configured ("2/8 configured"), and opens the register select and type list on a click or Enter. A page with several registers is a short card until the administrator opens the group they need.
117
117
 
118
+ ## Board look
119
+
120
+ In the board look (`look="board"`, or the `cnLook` that `CnAppRoot` or `CnAdminSettingsShell` provides) one group is open: its name, a status chip, the register select and a type/schema table with a check icon on each configured type. It lists `boardRowLimit` types and counts the rest in a line with a button that lists them all. The other groups are rows with their type count and a status chip; activating a row opens that group. The save button closes the section instead of sitting in its header.
121
+
122
+ ## Loading
123
+
124
+ The register select lists every register without its schemas. Only the registers the configuration maps have their schemas fetched (`registers/{id}/schemas`), and a register chosen in the select is fetched when chosen.
125
+
118
126
  ## Additional props
119
127
 
120
128
  | Prop | Type | Default | Description |
@@ -127,5 +135,7 @@ With more than one group each group starts closed: its heading is a button that
127
135
  | `saveButtonText` | String | `'Save configuration'` | Label for the save button |
128
136
  | `reimportButtonText` | String | `'Re-import configuration'` | Label for the reimport button |
129
137
  | `collapseGroups` | Boolean | `null` | Draw each group as a heading button that opens and closes it, with its configured count shown while closed; the groups start closed. `null` does this when there is more than one group, `true` or `false` forces it |
138
+ | `look` | String | `''` | `board` or `nextcloud`; empty follows the provided `cnLook` |
139
+ | `boardRowLimit` | Number | `4` | Board look: how many types the open group lists before the "show all" line; `0` lists every type |
130
140
  | `autoMatch` | Boolean | `true` | Automatically match schema titles to type slugs when a register is selected |
131
141
  | `labels` | Object | *(see below)* | UI label overrides for all user-visible strings (register, schema, notConfigured, noSchemas, selectRegister, selectSchema, allConfigured, partiallyConfigured) |