@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
@@ -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
  *
@@ -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 -->
@@ -74,6 +74,7 @@ import '../CnFlowRunsWidget/index.js'
74
74
  import '../CnTasksWidget/index.js'
75
75
  import '../CnSectionsWidget/index.js'
76
76
  import '../CnFactsWidget/index.js'
77
+ import '../CnDeviceFramesWidget/index.js'
77
78
  import '../CnFieldListWidget/index.js'
78
79
  import '../CnSettingsFormWidget/index.js'
79
80
  import '../CnSwitchRowsWidget/index.js'
@@ -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
+ ```
@@ -54,11 +54,14 @@
54
54
  class="cn-widget-wrapper__icon" />
55
55
  <!-- An eyebrow (`eyebrow`) is a muted line above the title:
56
56
  "Saldo per week" above "Prognose 13 weken". -->
57
- <div v-if="resolvedEyebrow" class="cn-widget-wrapper__title-stack">
58
- <span class="cn-widget-wrapper__eyebrow" data-testid="cn-widget-wrapper-eyebrow">{{ resolvedEyebrow }}</span>
57
+ <!-- A subtitle (`subtitle`) is a muted line under the title:
58
+ "Dit kwartaal, beslist binnen de termijn". -->
59
+ <div v-if="resolvedEyebrow || resolvedSubtitle" class="cn-widget-wrapper__title-stack">
60
+ <span v-if="resolvedEyebrow" class="cn-widget-wrapper__eyebrow" data-testid="cn-widget-wrapper-eyebrow">{{ resolvedEyebrow }}</span>
59
61
  <h3 :id="titleId" class="cn-widget-wrapper__title">
60
62
  {{ resolvedTitle }}
61
63
  </h3>
64
+ <span v-if="resolvedSubtitle" class="cn-widget-wrapper__subtitle" data-testid="cn-widget-wrapper-subtitle">{{ resolvedSubtitle }}</span>
62
65
  </div>
63
66
  <h3 v-else :id="titleId" class="cn-widget-wrapper__title">
64
67
  {{ resolvedTitle }}
@@ -264,6 +267,17 @@ export default {
264
267
  default: '',
265
268
  },
266
269
 
270
+ /**
271
+ * A muted line under the title, such as the period a chart covers.
272
+ * Run through the host translate function. Empty draws none.
273
+ *
274
+ * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-dashboard-widget-card-can-carry-a-line-under-its-title
275
+ */
276
+ subtitle: {
277
+ type: String,
278
+ default: '',
279
+ },
280
+
267
281
  /** Whether to show the header with title */
268
282
  showTitle: {
269
283
  type: Boolean,
@@ -733,6 +747,15 @@ export default {
733
747
  return this.eyebrow ? this.effectiveTranslate(this.eyebrow) : ''
734
748
  },
735
749
 
750
+ /**
751
+ * The subtitle run through the host translate function.
752
+ *
753
+ * @return {string}
754
+ */
755
+ resolvedSubtitle() {
756
+ return this.subtitle ? this.effectiveTranslate(this.subtitle) : ''
757
+ },
758
+
736
759
  /**
737
760
  * The RENDERED widget title — `displayTitle` run through the host
738
761
  * translate function. Kept separate from `displayTitle` on purpose:
@@ -1175,6 +1198,12 @@ export default {
1175
1198
  min-width: 0;
1176
1199
  }
1177
1200
 
1201
+ .cn-widget-wrapper__subtitle {
1202
+ font-size: 13px;
1203
+ line-height: 1.3;
1204
+ color: var(--color-text-maxcontrast);
1205
+ }
1206
+
1178
1207
  .cn-widget-wrapper__eyebrow {
1179
1208
  font-size: 13px;
1180
1209
  line-height: 1.3;
@@ -220,6 +220,7 @@ export { CnTasksWidget } from './CnTasksWidget/index.js'
220
220
  export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
221
221
  export { CnSectionsWidget } from './CnSectionsWidget/index.js'
222
222
  export { CnFactsWidget } from './CnFactsWidget/index.js'
223
+ export { CnDeviceFramesWidget } from './CnDeviceFramesWidget/index.js'
223
224
  export { CnFieldListWidget } from './CnFieldListWidget/index.js'
224
225
  export { CnSettingsFormWidget } from './CnSettingsFormWidget/index.js'
225
226
  export { CnSwitchRowsWidget } from './CnSwitchRowsWidget/index.js'
@@ -267,6 +267,15 @@ function tasksSource() {
267
267
  */
268
268
  loading: () => !!store.loading,
269
269
 
270
+ /**
271
+ * Count the inbox for a config without touching the rows on screen,
272
+ * for a quick-filter tab with `showCount`.
273
+ *
274
+ * @param {object} config The merged config, as for `load`.
275
+ * @return {Promise<number>} The datastore total.
276
+ */
277
+ count: async (config) => (await store.fetchFor({ ...(config || {}), limit: 1 })).total,
278
+
270
279
  columns: [
271
280
  { key: 'title', label: t('nextcloud-vue', 'Task') },
272
281
  { key: 'subjectLabel', label: t('nextcloud-vue', 'Subject') },
@@ -250,3 +250,17 @@
250
250
  min-height: 44px;
251
251
  }
252
252
  }
253
+
254
+ /* Above a phone the boards top-align a dialog (DqNieuweZaak, DqUwWachtrij,
255
+ DqVolgendeStap: the top edge sits about 90px down at 1440 wide) instead of
256
+ centring it. `--cn-dialog-top` moves it. */
257
+ @media (min-width: 601px) {
258
+ .modal-mask.cn-dialog-board .modal-wrapper {
259
+ align-items: flex-start;
260
+ }
261
+
262
+ .modal-mask.cn-dialog-board .modal-wrapper .modal-container {
263
+ margin-top: var(--cn-dialog-top, 88px);
264
+ max-height: calc(100% - var(--cn-dialog-top, 88px) - 24px);
265
+ }
266
+ }
@@ -481,10 +481,11 @@
481
481
  border-radius: 0;
482
482
  }
483
483
 
484
+ /* The header row is near-white on the boards (#fafbfc), not the hover grey. */
484
485
  .cn-look-board .cn-table-container--board .cn-data-table th {
485
486
  padding: 12px 16px;
486
487
  border-bottom: 0;
487
- background: var(--color-background-hover);
488
+ background: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
488
489
  color: var(--color-text-maxcontrast);
489
490
  font-weight: 600;
490
491
  }
@@ -542,6 +543,15 @@
542
543
  max-width: calc(100% - 26px);
543
544
  }
544
545
 
546
+ /* The board's funnel: a 22px button 4px after the sort chevron. */
547
+ .cn-look-board .cn-table-container--board .cn-data-table th .cn-table-header__filter {
548
+ width: 22px;
549
+ min-width: 22px;
550
+ height: 22px;
551
+ min-height: 22px;
552
+ margin-inline-start: 4px;
553
+ }
554
+
545
555
  /* The selection column: 18px checkboxes, 28px of content after 16px of padding.
546
556
  The box model is set with `!important` and the widths include the padding:
547
557
  Nextcloud's server.css sets `box-sizing: border-box` on everything under
@@ -747,6 +757,14 @@
747
757
  padding: 0 12px;
748
758
  }
749
759
 
760
+ /* The inert Next of a single page: a faint outline, muted text. */
761
+ .cn-look-board .cn-pagination--board .cn-pagination__board-link--disabled {
762
+ display: inline-flex;
763
+ align-items: center;
764
+ color: var(--color-text-maxcontrast);
765
+ cursor: default;
766
+ }
767
+
750
768
  .cn-look-board .cn-pagination--board .cn-pagination__board-link--current {
751
769
  border-color: var(--color-primary-element);
752
770
  background: var(--color-primary-element);
@@ -1028,3 +1046,11 @@
1028
1046
  padding: 0 12px;
1029
1047
  border-radius: 8px;
1030
1048
  }
1049
+
1050
+ /* The sidebar's own open toggle (NcAppSidebar's `.app-sidebar__toggle`, drawn
1051
+ when the sidebar is closed) sits over the content's top-right corner, which
1052
+ on an index page is the primary header button: a stray dark square on
1053
+ "New contact". The board draws none; the Filter button opens the sidebar. */
1054
+ .cn-look-board:has(.cn-index-page) > .app-sidebar__toggle {
1055
+ display: none;
1056
+ }
@@ -624,6 +624,33 @@
624
624
  opacity: 1 !important;
625
625
  }
626
626
 
627
+ /* The support card at the end of an admin page (`supportPosition: "end"`,
628
+ board PqBeheer): one row, the title then the lines, wrapping in a narrow
629
+ window, with less padding than a settings card.
630
+ @spec openspec/changes/admin-shell-support-end/specs/settings-board-look/spec.md#requirement-support-can-be-its-own-card-at-the-end */
631
+ .cn-look-board .settings-section.cn-admin-settings-shell__support:not(.cn-look-nextcloud *) {
632
+ flex-direction: row !important;
633
+ flex-wrap: wrap;
634
+ align-items: center;
635
+ gap: 10px 24px;
636
+ padding: 16px 24px;
637
+ }
638
+
639
+ .cn-look-board .cn-admin-settings-shell__support:not(.cn-look-nextcloud *) .settings-section__name {
640
+ font-size: 16px;
641
+ }
642
+
643
+ .cn-look-board .cn-admin-settings-shell__support:not(.cn-look-nextcloud *) .cn-settings-section__content {
644
+ display: flex;
645
+ flex-wrap: wrap;
646
+ gap: 10px 24px;
647
+ }
648
+
649
+ .cn-look-board .cn-admin-settings-shell__support:not(.cn-look-nextcloud *) .cn-admin-settings-shell__support-line {
650
+ margin: 0;
651
+ font-size: 14px;
652
+ }
653
+
627
654
  /* The section footer is the save row: the hairline replaces its own rule. */
628
655
  .cn-look-board .settings-section .cn-settings-section__footer {
629
656
  margin: 0;
@@ -61,3 +61,19 @@
61
61
  white-space: nowrap;
62
62
  border: 0;
63
63
  }
64
+
65
+ /* The breadcrumb and meta line under the title (CnHeaderTrail). In the board
66
+ look it takes the detail header row's styles; here it only lays the crumbs,
67
+ the dot and the note out on one line. */
68
+ .cn-header-trail {
69
+ align-items: center;
70
+ color: var(--color-text-maxcontrast);
71
+ display: flex;
72
+ flex-wrap: wrap;
73
+ gap: var(--default-grid-baseline);
74
+ margin-bottom: var(--default-grid-baseline);
75
+ }
76
+
77
+ .cn-header-trail .cn-detail-page__breadcrumbs {
78
+ margin-bottom: 0;
79
+ }
package/src/index.js CHANGED
@@ -99,6 +99,7 @@ export {
99
99
  CnDetailGrid,
100
100
  CnDetailPage,
101
101
  CnDetailWidgetHost,
102
+ CnDeviceFramesWidget,
102
103
  CnDialog,
103
104
  CnDialogHeader,
104
105
  CnDividerWidget,