@conduction/nextcloud-vue 2.68.0 → 2.70.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 (137) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +25 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +155 -152
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +77 -0
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +8 -8
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  9. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -0
  10. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js.map +1 -1
  11. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +28 -0
  12. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  13. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +4 -2
  14. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  15. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +25 -1
  16. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js +32 -17
  18. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js +14 -1
  20. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +43 -2
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  25. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -35
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +19 -1
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +35 -1
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +94 -6
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +64 -19
  36. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +28 -0
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +3 -2
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +12 -0
  42. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +9 -0
  44. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  46. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +54 -7
  47. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +26 -8
  49. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +143 -0
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +68 -25
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +4 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +17 -3
  60. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +25 -24
  62. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +29 -2
  66. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  67. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +7 -0
  68. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +24 -1
  70. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  71. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +1 -1
  72. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +81 -1
  74. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +25 -3
  76. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +15 -2
  78. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +31 -32
  80. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  81. package/dist/esm/l10n/en.json.js +3 -0
  82. package/dist/esm/l10n/en.json.js.map +1 -1
  83. package/dist/esm/l10n/nl.json.js +3 -0
  84. package/dist/esm/l10n/nl.json.js.map +1 -1
  85. package/dist/esm/mixins/pageViews.js +278 -11
  86. package/dist/esm/mixins/pageViews.js.map +1 -1
  87. package/dist/esm/nextcloud-vue.css +145 -0
  88. package/dist/esm/store/plugins/dashboardLayouts.js +35 -1
  89. package/dist/esm/store/plugins/dashboardLayouts.js.map +1 -1
  90. package/dist/esm/utils/capabilityComparison.js +29 -1
  91. package/dist/esm/utils/capabilityComparison.js.map +1 -1
  92. package/dist/esm/utils/formatMetric.js +137 -16
  93. package/dist/esm/utils/formatMetric.js.map +1 -1
  94. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  95. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  96. package/dist/nextcloud-vue.cjs.js +2840 -1477
  97. package/dist/nextcloud-vue.cjs.js.map +2 -2
  98. package/dist/nextcloud-vue.css +145 -0
  99. package/l10n/en.json +3 -0
  100. package/l10n/nl.json +3 -0
  101. package/package.json +1 -1
  102. package/src/components/CnActionsBar/CnActionsBar.md +2 -0
  103. package/src/components/CnActionsBar/CnActionsBar.vue +28 -2
  104. package/src/components/CnAppNav/CnAppNav.vue +85 -8
  105. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +45 -0
  106. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +32 -4
  107. package/src/components/CnCapabilityTable/CnCapabilityTable.vue +14 -0
  108. package/src/components/CnDashboardGrid/CnDashboardGrid.md +2 -0
  109. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +54 -2
  110. package/src/components/CnDashboardPage/CnDashboardPage.md +2 -0
  111. package/src/components/CnDashboardPage/CnDashboardPage.vue +59 -35
  112. package/src/components/CnDataTable/CnDataTable.vue +35 -1
  113. package/src/components/CnDetailPage/CnDetailPage.md +8 -0
  114. package/src/components/CnDetailPage/CnDetailPage.vue +124 -8
  115. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +30 -1
  116. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +12 -0
  117. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +9 -0
  118. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +95 -8
  119. package/src/components/CnIndexPage/CnIndexPage.vue +171 -6
  120. package/src/components/CnNextStepCard/CnNextStepCard.vue +11 -0
  121. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +4 -1
  122. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +34 -7
  123. package/src/components/CnStagesWidget/CnStagesWidget.vue +19 -0
  124. package/src/components/CnStatWidget/CnStatWidget.vue +38 -2
  125. package/src/components/CnStatsBlock/CnStatsBlock.md +2 -0
  126. package/src/components/CnStatsBlock/CnStatsBlock.vue +25 -2
  127. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +95 -3
  128. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +24 -3
  129. package/src/css/detail-page.css +10 -0
  130. package/src/css/index-page.css +12 -0
  131. package/src/css/kpi-card.css +15 -0
  132. package/src/mixins/pageViews.js +277 -10
  133. package/src/schemas/app-manifest-v2.schema.json +73 -6
  134. package/src/store/plugins/dashboardLayouts.js +34 -0
  135. package/src/utils/capabilityComparison.js +28 -0
  136. package/src/utils/formatMetric.js +143 -15
  137. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -85,7 +85,7 @@
85
85
  v-else-if="isData && schemaObject"
86
86
  :title="resolvedTitle"
87
87
  :chromeless="isBare"
88
- :showActions="!isBare"
88
+ :showActions="!isBare && actionsShown"
89
89
  :icon="widget.icon || null"
90
90
  :schema="schemaObject"
91
91
  :objectData="object"
@@ -165,6 +165,7 @@
165
165
  v-else-if="renderer && isContentOnly && !isBare"
166
166
  :title="widget.title || ''"
167
167
  titleIconPosition="left"
168
+ :showActions="actionsShown || catalogAddEnabled"
168
169
  :showRefresh="false"
169
170
  :showRequestFeature="false"
170
171
  class="cn-detail-page__catalog-card">
@@ -473,6 +474,21 @@ export default {
473
474
  default: null,
474
475
  },
475
476
 
477
+ /**
478
+ * Whether a widget on a card keeps its overflow Actions menu when its
479
+ * definition does not say (`showActions`). `false` (CnDetailPage
480
+ * `showWidgetActions: false`) drops it from the record's data cards; a
481
+ * definition with `showActions: true` keeps it, one with
482
+ * `showActions: false` drops it whatever this says. True by default,
483
+ * which is the menu every card had.
484
+ *
485
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-detail-page-can-drop-the-widget-actions-menu
486
+ */
487
+ showActions: {
488
+ type: Boolean,
489
+ default: true,
490
+ },
491
+
476
492
  /**
477
493
  * The consumer's component registry, consulted before the built-in
478
494
  * catalog so a custom widget type overrides a built-in (REQ-MVR-005).
@@ -839,6 +855,19 @@ export default {
839
855
  return this.content.allowCreate !== false
840
856
  },
841
857
 
858
+ /**
859
+ * Whether the widget's overflow Actions menu renders: the definition's
860
+ * own `showActions` when it is a boolean, else the surface's
861
+ * `showActions`.
862
+ *
863
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-detail-page-can-drop-the-widget-actions-menu
864
+ * @return {boolean} true when the menu renders.
865
+ */
866
+ actionsShown() {
867
+ const own = this.widget && this.widget.showActions
868
+ return typeof own === 'boolean' ? own : this.showActions
869
+ },
870
+
842
871
  /**
843
872
  * The card header rule, with the surface's layout-driven answer winning
844
873
  * when it supplied one.
@@ -40,6 +40,7 @@
40
40
  // unconditionally.
41
41
  import { loadState } from '@nextcloud/initial-state'
42
42
  import CnFeaturesAndRoadmapView from '../CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue'
43
+ import { warnCapabilityComparisonDeprecated } from '../../utils/capabilityComparison.js'
43
44
  import { DEFAULT_FORGE } from '../../utils/forge.js'
44
45
 
45
46
  /**
@@ -175,6 +176,11 @@ export default {
175
176
  * Manifest config > initialState (`features_roadmap_capabilities`) >
176
177
  * null. When null the view renders its two stops as it always has.
177
178
  *
179
+ * Deprecated: publish the comparison on the app's docs site and link to it (as dossiq #3312 and pipelinq #2195 did). Still rendered; removal waits for a major version. The same goes for the `features_roadmap_capabilities` initial
180
+ * state key. A development build warns once when either is set.
181
+ *
182
+ * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.
183
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated
178
184
  * @type {object|null}
179
185
  */
180
186
  capabilityComparison: {
@@ -298,5 +304,11 @@ export default {
298
304
  return readInitialState(this.effectiveAppId, 'features_roadmap_capabilities', null)
299
305
  },
300
306
  },
307
+
308
+ mounted() {
309
+ if (this.resolvedCapabilityComparison) {
310
+ warnCapabilityComparisonDeprecated('CnFeaturesAndRoadmapPage `capabilityComparison` / the `features_roadmap_capabilities` initial state')
311
+ }
312
+ },
301
313
  }
302
314
  </script>
@@ -114,6 +114,7 @@ import CnFeaturesAndRoadmapSidebar from '../CnFeaturesAndRoadmapSidebar/CnFeatur
114
114
  import CnFeaturesTab from '../CnFeaturesTab/CnFeaturesTab.vue'
115
115
  import CnRoadmapTab from '../CnRoadmapTab/CnRoadmapTab.vue'
116
116
  import CnSupportDialog from '../CnSupportDialog/CnSupportDialog.vue'
117
+ import { warnCapabilityComparisonDeprecated } from '../../utils/capabilityComparison.js'
117
118
  import { buildFeatureRequestUrl, DEFAULT_FORGE } from '../../utils/forge.js'
118
119
 
119
120
  const DEFAULT_OPENBUILT_PATH = '/apps/openbuilt'
@@ -214,6 +215,11 @@ export default {
214
215
  * When null (default) the toggle keeps its two stops and the view
215
216
  * renders exactly as it does for every app that passes nothing.
216
217
  *
218
+ * Deprecated: publish the comparison on the app's docs site and link to it (as dossiq #3312 and pipelinq #2195 did). Still rendered; removal waits for a major version.
219
+ * A development build warns once when it is set.
220
+ *
221
+ * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.
222
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated
217
223
  * @type {object|null}
218
224
  */
219
225
  capabilityComparison: {
@@ -527,6 +533,9 @@ export default {
527
533
 
528
534
  mounted() {
529
535
  this.publishHoistedSidebar()
536
+ if (this.capabilityComparison) {
537
+ warnCapabilityComparisonDeprecated('CnFeaturesAndRoadmapView `capabilityComparison`')
538
+ }
530
539
  },
531
540
 
532
541
  beforeUnmount() {
@@ -4,12 +4,25 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--ground': isGround, 'cn-header-widget--with-views': viewOptions.length > 0 }" :style="wrapperStyle">
7
+ <div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--ground': isGround, 'cn-header-widget--with-views': viewOptions.length > 0, 'cn-header-widget--with-emblem': emblem !== null }" :style="wrapperStyle">
8
8
  <div
9
9
  v-if="hasOverlay"
10
10
  class="cn-header-widget__overlay"
11
11
  :style="overlayStyle"
12
12
  aria-hidden="true" />
13
+ <!-- The emblem beside the greeting (`content.emblem`): a URL, or
14
+ `true` for the theme's emblem (--nldesign-emblem-url). Decoration. -->
15
+ <img
16
+ v-if="emblem && emblem !== true"
17
+ class="cn-header-widget__emblem"
18
+ :src="emblem"
19
+ alt=""
20
+ data-testid="cn-header-widget-emblem">
21
+ <span
22
+ v-else-if="emblem === true"
23
+ class="cn-header-widget__emblem cn-header-widget__emblem--theme"
24
+ aria-hidden="true"
25
+ data-testid="cn-header-widget-emblem" />
13
26
  <div class="cn-header-widget__content" :style="contentStyle">
14
27
  <p
15
28
  v-if="dateLine"
@@ -140,7 +153,7 @@ export default {
140
153
  * Persisted widget content: `{title, subtitle, backgroundImageUrl,
141
154
  * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
142
155
  * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
143
- * greeting, showDate, plain, ground, views}`. All fields are optional except
156
+ * greeting, showDate, kicker, emblem, plain, ground, views}`. All fields are optional except
144
157
  * `title` (or `greeting`); unknown enum values collapse to documented
145
158
  * defaults and the renderer never throws. `views` is
146
159
  * `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:
@@ -151,7 +164,12 @@ export default {
151
164
  * own. `ground: true`
152
165
  * draws the greeting on the page ground: the plain look with no
153
166
  * padding, a 32px heading and the date line 6px above it (a dashboard
154
- * also drops the widget's card for it).
167
+ * also drops the widget's card for it). A ground greeting takes its
168
+ * own height rather than its grid cell's, so a view switch sits on
169
+ * the heading's line. `kicker` (an i18n key) prefixes the date line, as
170
+ * "Customer contact · Tuesday 6 October". `emblem` (a URL, or `true`
171
+ * for the theme's `--nldesign-emblem-url`) draws the emblem beside the
172
+ * date line and heading, 52px high (`--cn-header-emblem-size`).
155
173
  *
156
174
  * @type {object}
157
175
  */
@@ -396,13 +414,30 @@ export default {
396
414
  },
397
415
 
398
416
  /**
399
- * Today's date written out ("Monday 5 October 2026"), or '' when
400
- * `showDate` is off.
417
+ * The kicker line above the heading: `content.kicker` (through the
418
+ * host translate function, e.g. "Customer contact") and today's date
419
+ * written out ("Monday 5 October 2026") when `showDate` is on, joined
420
+ * by " · ". '' when neither is set. Without `kicker` this is the date
421
+ * line as before.
401
422
  *
402
423
  * @return {string}
403
424
  * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header
404
425
  */
405
426
  dateLine() {
427
+ const kicker = this.content && typeof this.content.kicker === 'string' && this.content.kicker !== ''
428
+ ? this.effectiveTranslate(this.content.kicker)
429
+ : ''
430
+ const date = this.dateText
431
+ return [kicker, date].filter((part) => part !== '').join(' · ')
432
+ },
433
+
434
+ /**
435
+ * Today's date written out, or '' when `showDate` is off.
436
+ *
437
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-greeting-kicker-can-carry-a-prefix
438
+ * @return {string}
439
+ */
440
+ dateText() {
406
441
  if (!this.content || this.content.showDate !== true) {
407
442
  return ''
408
443
  }
@@ -434,6 +469,21 @@ export default {
434
469
  }
435
470
  },
436
471
 
472
+ /**
473
+ * The emblem beside the greeting: the URL `content.emblem` names,
474
+ * `true` for the theme's emblem, or null without the key.
475
+ *
476
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-greeting-can-carry-the-emblem
477
+ * @return {string|true|null}
478
+ */
479
+ emblem() {
480
+ const value = this.content && this.content.emblem
481
+ if (value === true) {
482
+ return true
483
+ }
484
+ return typeof value === 'string' && value !== '' ? resolveImageUrl(value) : null
485
+ },
486
+
437
487
  /** Whether a non-empty subtitle is set. */
438
488
  hasSubtitle() {
439
489
  return this.subtitle !== ''
@@ -574,7 +624,12 @@ export default {
574
624
  return this.isLightColor(this.backgroundColor) ? '#000000' : '#ffffff'
575
625
  },
576
626
 
577
- /** Inline style for the banner wrapper. */
627
+ /**
628
+ * Inline style for the banner wrapper.
629
+ *
630
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-ground-greeting-lines-its-switch-up-with-the-heading
631
+ * @return {object}
632
+ */
578
633
  wrapperStyle() {
579
634
  const style = {
580
635
  position: 'relative',
@@ -583,7 +638,11 @@ export default {
583
638
  // widget. A fixed pixel height ignored the cell, which made the
584
639
  // banner un-resizable and made it overflow/scroll inside a
585
640
  // smaller cell. The grid controls the size now.
586
- height: '100%',
641
+ // On the page ground there is no banner to fill the cell with:
642
+ // the greeting takes its own height, so the view switch lines
643
+ // up with the heading instead of the bottom of a taller cell,
644
+ // and a size-to-content cell can shrink to it.
645
+ height: this.isGround ? 'auto' : '100%',
587
646
  overflow: 'hidden',
588
647
  'background-color': this.backgroundColor,
589
648
  }
@@ -619,6 +678,7 @@ export default {
619
678
  * Inline style for the content flex container.
620
679
  *
621
680
  * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header
681
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-ground-greeting-lines-its-switch-up-with-the-heading
622
682
  */
623
683
  contentStyle() {
624
684
  return {
@@ -627,7 +687,7 @@ export default {
627
687
  // Beside a view switch the content shares the row instead of
628
688
  // taking the whole width (see .cn-header-widget--with-views).
629
689
  width: this.viewOptions.length > 0 ? 'auto' : '100%',
630
- height: '100%',
690
+ height: this.isGround ? 'auto' : '100%',
631
691
  display: 'flex',
632
692
  'flex-direction': 'column',
633
693
  'align-items': this.flexAlignFromTextAlign,
@@ -834,6 +894,33 @@ export default {
834
894
  width: auto;
835
895
  }
836
896
 
897
+ /* The emblem beside the greeting (`content.emblem`): on one row with the
898
+ date line and heading, centred on them, as LpStart draws it. */
899
+ .cn-header-widget--with-emblem {
900
+ display: flex;
901
+ column-gap: var(--cn-header-emblem-gap, 16px);
902
+ }
903
+
904
+ .cn-header-widget--with-emblem .cn-header-widget__content {
905
+ flex: 1 1 auto;
906
+ width: auto;
907
+ min-width: 0;
908
+ }
909
+
910
+ .cn-header-widget__emblem {
911
+ position: relative;
912
+ z-index: 1;
913
+ flex: none;
914
+ align-self: center;
915
+ height: var(--cn-header-emblem-size, 52px);
916
+ width: auto;
917
+ }
918
+
919
+ .cn-header-widget__emblem--theme {
920
+ width: var(--cn-header-emblem-size, 52px);
921
+ background: var(--nldesign-emblem-url) center / contain no-repeat;
922
+ }
923
+
837
924
  .cn-header-widget__views {
838
925
  position: relative;
839
926
  z-index: 1;
@@ -19,8 +19,28 @@
19
19
  <CnPageHeader
20
20
  :title="title"
21
21
  :description="headerDescription"
22
- :icon="resolvedIcon"
23
- :visuallyHidden="!showTitle" />
22
+ :icon="showTitleIcon ? resolvedIcon : ''"
23
+ :visuallyHidden="!showTitle">
24
+ <!-- The board's header buttons (`headerButtons`), beside the
25
+ title. Declaring them takes the Views and Actions menus
26
+ (and the Add / Export controls a button takes over) out of
27
+ the actions bar. -->
28
+ <template v-if="headerButtonsShown" #extra>
29
+ <div class="cn-index-page__header-buttons" data-testid="cn-index-header-buttons">
30
+ <NcButton
31
+ v-for="button in resolvedHeaderButtons"
32
+ :key="button.key"
33
+ :variant="button.variant"
34
+ :data-testid="`cn-index-header-button-${button.key}`"
35
+ @click="onHeaderButton(button)">
36
+ <template v-if="button.icon" #icon>
37
+ <CnIcon :name="button.icon" :size="20" />
38
+ </template>
39
+ {{ button.label }}
40
+ </NcButton>
41
+ </div>
42
+ </template>
43
+ </CnPageHeader>
24
44
  </slot>
25
45
 
26
46
  <!-- Optional content below header, above actions bar -->
@@ -64,7 +84,9 @@
64
84
  :refreshDisabled="refreshDisabled"
65
85
  :addDisabled="addDisabled"
66
86
  :addTo="addLinkTo"
67
- :showAdd="effectiveShowAdd"
87
+ :showAdd="effectiveShowAdd && !headerButtonTakes('add')"
88
+ :showCount="showCount"
89
+ :showActionsMenu="!headerButtonsShown"
68
90
  :showSidebarToggle="hasSidebar"
69
91
  :sidebarOpen="sidebarOpen"
70
92
  :headerActions="mergedHeaderActions"
@@ -111,12 +133,12 @@
111
133
  views, export, page config — grouped together and kept apart from
112
134
  the app-specific buttons in `#actions`, with Add sitting between
113
135
  the two groups rather than before both (dossiq Cases/Queue). -->
114
- <template v-if="isEditMode || showExportMenu || allowSavedViews" #actions-end>
136
+ <template v-if="isEditMode || barShowsExportMenu || barShowsSavedViews" #actions-end>
115
137
  <!-- Saved views (opt-in via `allowSavedViews`): lists the user's
116
138
  OpenRegister saved-search views; applying one writes its stored
117
139
  filters/search/sort into the route query. -->
118
140
  <CnSavedViewsControl
119
- v-if="allowSavedViews"
141
+ v-if="barShowsSavedViews"
120
142
  :views="viewsForControl"
121
143
  :loading="savedViewsLoading"
122
144
  :currentUserId="currentSavedViewsUserId"
@@ -128,7 +150,7 @@
128
150
  CSV/Excel entries navigate to OR's export-leaf URL, passing the
129
151
  current route's query params through as filters. -->
130
152
  <NcActions
131
- v-if="showExportMenu"
153
+ v-if="barShowsExportMenu"
132
154
  :forceName="true"
133
155
  :menuName="t('nextcloud-vue', 'Export')"
134
156
  data-testid="cn-index-export-menu"
@@ -1225,6 +1247,51 @@ export default {
1225
1247
  default: false,
1226
1248
  },
1227
1249
 
1250
+ /**
1251
+ * Whether the page header draws its icon before the title. `false`
1252
+ * (manifest `config.showTitleIcon: false`) drops it, for a title that
1253
+ * stands alone as the board draws it. True by default.
1254
+ *
1255
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
1256
+ */
1257
+ showTitleIcon: {
1258
+ type: Boolean,
1259
+ default: true,
1260
+ },
1261
+
1262
+ /**
1263
+ * Whether the actions bar shows its "Showing 20 of 258" line.
1264
+ * `false` (manifest `config.showCount: false`) drops it, for a page
1265
+ * whose title line already gives the total (`countSubtitle`). True by
1266
+ * default.
1267
+ *
1268
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
1269
+ */
1270
+ showCount: {
1271
+ type: Boolean,
1272
+ default: true,
1273
+ },
1274
+
1275
+ /**
1276
+ * Buttons beside the page title, as the board's "Export" and "New
1277
+ * case" (manifest `config.headerButtons`). Each is `{ label?, action,
1278
+ * variant?, icon?, format?, id? }`: `action` is `add` (the Add flow;
1279
+ * its label defaults to the Add label), `export` (the export leaf in
1280
+ * `format`, `csv` by default, else the export dialog), `import`,
1281
+ * `refresh`, or the id of a `headerActions` entry; `variant` is
1282
+ * `primary` or `secondary` (the default). Shown only with
1283
+ * `showTitle`. When they show, the actions bar drops its Views and
1284
+ * Actions menus, and the Add button and Export menu when a button
1285
+ * takes their action. Empty (the default) changes nothing.
1286
+ *
1287
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
1288
+ * @type {Array<{label?: string, action: string, variant?: ('primary'|'secondary'), icon?: string, format?: ('csv'|'excel'), id?: string}>}
1289
+ */
1290
+ headerButtons: {
1291
+ type: Array,
1292
+ default: () => [],
1293
+ },
1294
+
1228
1295
  /** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */
1229
1296
  icon: {
1230
1297
  type: String,
@@ -4284,6 +4351,67 @@ export default {
4284
4351
  return resolveDeepTokens(seed, {})
4285
4352
  },
4286
4353
 
4354
+ /**
4355
+ * The declared header buttons with a label, translated, each with a
4356
+ * stable key and a variant.
4357
+ *
4358
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
4359
+ * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
4360
+ */
4361
+ resolvedHeaderButtons() {
4362
+ const declared = Array.isArray(this.headerButtons) ? this.headerButtons : []
4363
+ return declared
4364
+ .filter((button) => button && typeof button === 'object' && typeof button.action === 'string' && button.action !== '')
4365
+ .map((button, index) => {
4366
+ let label = typeof button.label === 'string' && button.label !== '' ? this.cnTranslate(button.label) : ''
4367
+ if (label === '' && button.action === 'add') {
4368
+ label = this.resolvedAddLabel
4369
+ }
4370
+ return {
4371
+ key: String(button.id || button.action || index),
4372
+ label,
4373
+ action: button.action,
4374
+ variant: button.variant === 'primary' ? 'primary' : 'secondary',
4375
+ icon: typeof button.icon === 'string' ? button.icon : '',
4376
+ format: button.format === 'excel' ? 'excel' : 'csv',
4377
+ }
4378
+ })
4379
+ .filter((button) => button.label !== '')
4380
+ },
4381
+
4382
+ /**
4383
+ * Whether the header buttons render: there are some, the title shows
4384
+ * (they sit beside it) and no `#header` slot replaces the header.
4385
+ *
4386
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
4387
+ * @return {boolean}
4388
+ */
4389
+ headerButtonsShown() {
4390
+ return this.showTitle && !this.$slots.header && this.resolvedHeaderButtons.length > 0
4391
+ },
4392
+
4393
+ /**
4394
+ * Whether the actions bar shows the saved-views control: opted in
4395
+ * with `allowSavedViews` and not given up for header buttons.
4396
+ *
4397
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
4398
+ * @return {boolean}
4399
+ */
4400
+ barShowsSavedViews() {
4401
+ return Boolean(this.allowSavedViews) && !this.headerButtonsShown
4402
+ },
4403
+
4404
+ /**
4405
+ * Whether the actions bar shows the Export menu: `showExportMenu`,
4406
+ * unless a header button took the export over.
4407
+ *
4408
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
4409
+ * @return {boolean}
4410
+ */
4411
+ barShowsExportMenu() {
4412
+ return this.showExportMenu && !this.headerButtonTakes('export')
4413
+ },
4414
+
4287
4415
  /**
4288
4416
  * The header's description line: `countSubtitle` with the total
4289
4417
  * filled in when one is known, else `description`.
@@ -5086,6 +5214,43 @@ export default {
5086
5214
  this.$emit('open-modal', { target, props: { selectedIds, count, ...own } })
5087
5215
  },
5088
5216
 
5217
+ /**
5218
+ * Whether a shown header button takes `action` over from the bar.
5219
+ *
5220
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
5221
+ * @param {string} action The action, e.g. `add`.
5222
+ * @return {boolean}
5223
+ */
5224
+ headerButtonTakes(action) {
5225
+ return this.headerButtonsShown && this.resolvedHeaderButtons.some((button) => button.action === action)
5226
+ },
5227
+
5228
+ /**
5229
+ * A header button was clicked: run its built-in action, or dispatch
5230
+ * it as the `headerActions` entry with that id.
5231
+ *
5232
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
5233
+ * @param {{action: string, format: string}} button The resolved button.
5234
+ * @return {void}
5235
+ */
5236
+ onHeaderButton(button) {
5237
+ if (button.action === 'add') {
5238
+ this.onAddClick()
5239
+ } else if (button.action === 'export') {
5240
+ if (this.register && this.exportSchemaSlug) {
5241
+ this.onExportClick(button.format)
5242
+ } else {
5243
+ this.showExportDialog = true
5244
+ }
5245
+ } else if (button.action === 'import') {
5246
+ this.showImportDialog = true
5247
+ } else if (button.action === 'refresh') {
5248
+ this.onRefreshEvent()
5249
+ } else {
5250
+ this.onHeaderAction({ action: button.action, id: button.action })
5251
+ }
5252
+ },
5253
+
5089
5254
  /**
5090
5255
  * Click dispatch from CnActionsBar's `@header-action`. Looks
5091
5256
  * up the resolved entry by id, invokes its handler if any,
@@ -241,6 +241,17 @@ export default {
241
241
  text-transform: uppercase;
242
242
  }
243
243
 
244
+ /* The title is a small kicker, not a section heading, but it is an <h3> (or
245
+ whatever `titleTag` says) for the outline, and a theme that sizes every
246
+ heading with !important (thematiq's nldesign sheet: `h3 { font-size:
247
+ var(--nldesign-component-heading-3-font-size) !important }`) blew it up to
248
+ an 18px uppercase heading. Only !important beats !important; the doubled
249
+ class makes this the more specific rule. Without such a theme the size is
250
+ the 0.85em above, unchanged. */
251
+ .cn-next-step-card__title.cn-next-step-card__title {
252
+ font-size: 0.85em !important;
253
+ }
254
+
244
255
  .cn-next-step-card__list {
245
256
  display: grid;
246
257
  gap: calc(2 * var(--default-grid-baseline));
@@ -614,7 +614,10 @@ export default {
614
614
  return { key: c, label: headingFor(c) }
615
615
  }
616
616
  const out = { key: c.key, label: c.label || headingFor(c.key) }
617
- for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable']) {
617
+ // `secondary` (a second, muted line under the cell) rides
618
+ // along too: the table draws it, and dropping it here took the
619
+ // list rows' second line away (zuiddrecht-pixel-gaps-3).
620
+ for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary']) {
618
621
  if (c[k] !== undefined) {
619
622
  out[k] = c[k]
620
623
  }
@@ -3,7 +3,7 @@
3
3
  SPDX-License-Identifier: EUPL-1.2
4
4
  -->
5
5
  <template>
6
- <div class="cn-stacked-bar" data-testid="cn-stacked-bar">
6
+ <div class="cn-stacked-bar" :class="{ 'cn-stacked-bar--inset': inset }" data-testid="cn-stacked-bar">
7
7
  <div v-if="loading" class="cn-stacked-bar__state" role="status">
8
8
  <NcLoadingIcon :size="20" />
9
9
  <span>{{ loadingLabel }}</span>
@@ -11,9 +11,14 @@
11
11
  <p v-else-if="error" class="cn-stacked-bar__state" role="status">
12
12
  {{ errorLabel }}
13
13
  </p>
14
- <p v-else-if="total === 0" class="cn-stacked-bar__state" data-testid="cn-stacked-bar-empty">
15
- {{ emptyLabel }}
16
- </p>
14
+ <!-- The designed empty state the list widgets use, compact: a bare
15
+ line of text read as a broken widget in an empty card. -->
16
+ <CnWidgetEmptyState
17
+ v-else-if="total === 0"
18
+ :name="emptyLabel"
19
+ compact
20
+ class="cn-stacked-bar__empty"
21
+ data-testid="cn-stacked-bar-empty" />
17
22
  <template v-else>
18
23
  <!-- The bar is a picture of the legend below it. The legend holds
19
24
  every label and number, so the bar itself is hidden from
@@ -50,6 +55,7 @@
50
55
  <script>
51
56
  import { translate as t } from '@nextcloud/l10n'
52
57
  import { NcLoadingIcon } from '@nextcloud/vue'
58
+ import CnWidgetEmptyState from '../CnWidgetEmptyState/CnWidgetEmptyState.vue'
53
59
  import { fetchGroupedCounts } from '../../utils/fetchAggregate.js'
54
60
 
55
61
  /** The lightest step keeps this share of the primary colour, so it still stands out from the page. */
@@ -114,7 +120,7 @@ export function rampColor(index, count) {
114
120
  export default {
115
121
  name: 'CnStackedBarWidget',
116
122
 
117
- components: { NcLoadingIcon },
123
+ components: { CnWidgetEmptyState, NcLoadingIcon },
118
124
 
119
125
  inject: {
120
126
  /**
@@ -133,9 +139,11 @@ export default {
133
139
  * group keys in the order they render; groups it does not list follow
134
140
  * in the order they arrive, and a listed key without records renders
135
141
  * with a count of 0. `labels` maps a group key to its label.
136
- * `emptyText` is shown when there is nothing to count.
142
+ * `emptyText` is shown when there is nothing to count. `inset: true`
143
+ * draws the bar and legend inside the board's inset (16px above, 24px
144
+ * at the sides, 22px below) instead of from card edge to card edge.
137
145
  *
138
- * @type {{source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}
146
+ * @type {{inset?: boolean, source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}
139
147
  */
140
148
  content: {
141
149
  type: Object,
@@ -163,6 +171,17 @@ export default {
163
171
  },
164
172
 
165
173
  computed: {
174
+ /**
175
+ * Whether the widget draws the board's inset (`content.inset`). Off
176
+ * by default, which keeps it edge to edge as before.
177
+ *
178
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-strip-and-a-stacked-bar-can-take-the-board-inset
179
+ * @return {boolean}
180
+ */
181
+ inset() {
182
+ return Boolean(this.content && this.content.inset === true)
183
+ },
184
+
166
185
  /**
167
186
  * The translate function in use.
168
187
  *
@@ -353,6 +372,14 @@ export default {
353
372
  width: 100%;
354
373
  }
355
374
 
375
+ /* `content.inset: true`: the board's inset inside the card (zuiddrecht-pixel-
376
+ gaps-3). A dashboard renders its widgets flush, so without it the strip ran
377
+ from card edge to card edge. Theme hook: --cn-widget-board-inset. */
378
+ .cn-stacked-bar--inset {
379
+ box-sizing: border-box;
380
+ padding: var(--cn-widget-board-inset, 16px 24px 22px);
381
+ }
382
+
356
383
  .cn-stacked-bar__state {
357
384
  display: flex;
358
385
  align-items: center;
@@ -1502,6 +1502,25 @@ button.cn-stages-widget__bar-label {
1502
1502
  cursor: pointer;
1503
1503
  }
1504
1504
 
1505
+ /* The same label under a theme that restyles every button with !important
1506
+ (thematiq's nldesign sheet: `button:not(.action-button)` gets 8px 16px
1507
+ padding, `min-width: max-content` and `overflow: visible`), and under core's
1508
+ `margin: 3px 3px 3px 0`, which outranks the scoped base rule. Together they
1509
+ put each clickable label about 10px below the current step's <span> and let
1510
+ a long label run over its neighbours, so eight Woo steps overlapped instead
1511
+ of ending in an ellipsis. Only !important beats !important; this selector is
1512
+ the more specific of the two. Without such a theme nothing changes: these
1513
+ are the values the base rule already sets. */
1514
+ button.cn-stages-widget__bar-label.cn-stages-widget__bar-label {
1515
+ min-width: 0 !important;
1516
+ margin: 0 !important;
1517
+ padding: 0 !important;
1518
+ border-width: 0 !important;
1519
+ overflow: hidden !important;
1520
+ text-overflow: ellipsis !important;
1521
+ white-space: nowrap !important;
1522
+ }
1523
+
1505
1524
  button.cn-stages-widget__bar-label[aria-disabled="true"] {
1506
1525
  cursor: not-allowed;
1507
1526
  }