@conduction/nextcloud-vue 2.56.0-beta.18 → 2.56.0-beta.19

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 (121) 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 +25 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -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 +54 -3
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -3
  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 +27 -0
  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 +11 -0
  74. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +4 -2
  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/nextcloud-vue.css +145 -0
  82. package/dist/esm/utils/capabilityComparison.js +29 -1
  83. package/dist/esm/utils/capabilityComparison.js.map +1 -1
  84. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  85. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  86. package/dist/nextcloud-vue.cjs.js +2167 -1399
  87. package/dist/nextcloud-vue.cjs.js.map +2 -2
  88. package/dist/nextcloud-vue.css +145 -0
  89. package/package.json +1 -1
  90. package/src/components/CnActionsBar/CnActionsBar.md +2 -0
  91. package/src/components/CnActionsBar/CnActionsBar.vue +28 -2
  92. package/src/components/CnAppNav/CnAppNav.vue +85 -8
  93. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +45 -0
  94. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +32 -4
  95. package/src/components/CnCapabilityTable/CnCapabilityTable.vue +14 -0
  96. package/src/components/CnDashboardGrid/CnDashboardGrid.md +2 -0
  97. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +54 -2
  98. package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
  99. package/src/components/CnDataTable/CnDataTable.vue +35 -1
  100. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  101. package/src/components/CnDetailPage/CnDetailPage.vue +57 -3
  102. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +30 -1
  103. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +12 -0
  104. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +9 -0
  105. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +95 -8
  106. package/src/components/CnIndexPage/CnIndexPage.vue +171 -6
  107. package/src/components/CnNextStepCard/CnNextStepCard.vue +11 -0
  108. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +4 -1
  109. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +34 -7
  110. package/src/components/CnStagesWidget/CnStagesWidget.vue +19 -0
  111. package/src/components/CnStatWidget/CnStatWidget.vue +36 -0
  112. package/src/components/CnStatsBlock/CnStatsBlock.md +2 -0
  113. package/src/components/CnStatsBlock/CnStatsBlock.vue +25 -2
  114. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +13 -0
  115. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +24 -3
  116. package/src/css/detail-page.css +10 -0
  117. package/src/css/index-page.css +12 -0
  118. package/src/css/kpi-card.css +15 -0
  119. package/src/schemas/app-manifest-v2.schema.json +46 -5
  120. package/src/utils/capabilityComparison.js +28 -0
  121. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -31,6 +31,7 @@
31
31
  v-for="(item, index) in layout"
32
32
  :key="resolveItemKey(item)"
33
33
  class="grid-stack-item"
34
+ :class="{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }"
34
35
  role="group"
35
36
  :aria-label="resolveItemLabel(item, index)"
36
37
  :aria-describedby="itemDescribedBy"
@@ -255,6 +256,19 @@ export default {
255
256
  type: Boolean,
256
257
  default: true,
257
258
  },
259
+
260
+ /**
261
+ * GridStack's `float`. True (the default) keeps every item on the row
262
+ * it was placed on, with empty rows above it if need be. False packs
263
+ * items upward, so a size-to-content cell that shrinks, or a widget
264
+ * that is not shown, closes its gap instead of leaving a band.
265
+ *
266
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
267
+ */
268
+ float: {
269
+ type: Boolean,
270
+ default: true,
271
+ },
258
272
  },
259
273
 
260
274
  emits: [
@@ -278,6 +292,8 @@ export default {
278
292
  return {
279
293
  grid: null,
280
294
  contentObserver: null,
295
+ /** True while the grid moves an item to where the layout put it. */
296
+ applyingLayout: false,
281
297
  domId: `cn-dashboard-grid-${++domIdCounter}`,
282
298
  /** Text currently held in the polite live region. */
283
299
  announcement: '',
@@ -485,7 +501,7 @@ export default {
485
501
  column: this.columns,
486
502
  cellHeight: this.cellHeight,
487
503
  margin: this.margin,
488
- float: true,
504
+ float: this.float,
489
505
  animate: true,
490
506
  disableDrag: !this.editable,
491
507
  disableResize: !this.editable,
@@ -558,6 +574,10 @@ export default {
558
574
  // switched off as the grid narrows, and back on as it widens.
559
575
  this.syncInteractivity()
560
576
 
577
+ if (this.applyingLayout) {
578
+ return
579
+ }
580
+
561
581
  // GridStack fires `change` for its own responsive rescale too, and at a
562
582
  // 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting
563
583
  // that has the host persist the reflow AS the authored layout, which
@@ -957,7 +977,29 @@ export default {
957
977
  const node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))
958
978
  if (!node) {
959
979
  this.grid.makeWidget(el)
960
- } else if (node.el !== el) {
980
+ } else if (node.el === el) {
981
+ // The layout MOVED an item the grid already tracks: the
982
+ // page re-compacts its display layout when a widget's
983
+ // condition hides it, and the item below was given a new
984
+ // gridY that never reached GridStack, so a band stayed
985
+ // where the hidden widget had been. A drag reports the
986
+ // grid's own position back, which matches and does
987
+ // nothing here.
988
+ const x = item.gridX ?? node.x
989
+ const y = item.gridY ?? node.y
990
+ if (x !== node.x || y !== node.y) {
991
+ // The layout said where it goes; GridStack's `change`
992
+ // for it is not a user's arrangement and is not
993
+ // reported back (it would write the display-only
994
+ // compaction into the authored layout).
995
+ this.applyingLayout = true
996
+ try {
997
+ this.grid.update(el, { x, y })
998
+ } finally {
999
+ this.applyingLayout = false
1000
+ }
1001
+ }
1002
+ } else {
961
1003
  // Drop the stale node without touching its detached DOM,
962
1004
  // then adopt the new element so GridStack sizes/places it.
963
1005
  this.grid.removeWidget(node.el, false, false)
@@ -1016,6 +1058,16 @@ export default {
1016
1058
  border-radius: var(--border-radius-container-large, 16px);
1017
1059
  }
1018
1060
 
1061
+ /* A size-to-content cell (`sizeToContent`) is measured by GridStack from the
1062
+ content's first child, and a widget wrapper there filled the cell
1063
+ (`height: 100%`), so the measure returned the cell's own height and the
1064
+ cell never shrank: "Waiting for me" stayed a tall card around one row. In
1065
+ such a cell the child takes its own height. A cell without the key keeps
1066
+ the filling child. */
1067
+ .cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {
1068
+ height: auto;
1069
+ }
1070
+
1019
1071
  :deep(.grid-stack-item-content) {
1020
1072
  background: var(--color-main-background);
1021
1073
  border: none;
@@ -305,6 +305,7 @@
305
305
  :columns="columns"
306
306
  :cellHeight="cellHeight"
307
307
  :margin="gridMargin"
308
+ :float="gridFloat"
308
309
  @layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
309
310
  <template #widget="{ item }">
310
311
  <!-- In-app edit overlay (ADR-041): a single launchpad-style
@@ -1030,6 +1031,19 @@ export default {
1030
1031
  default: true,
1031
1032
  },
1032
1033
 
1034
+ /**
1035
+ * GridStack's `float` for this dashboard (manifest
1036
+ * `config.gridFloat`). True (the default) keeps every widget on its
1037
+ * row. False packs widgets upward, so a size-to-content widget that
1038
+ * shrinks closes the gap under it instead of leaving an empty band.
1039
+ *
1040
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
1041
+ */
1042
+ gridFloat: {
1043
+ type: Boolean,
1044
+ default: true,
1045
+ },
1046
+
1033
1047
  /**
1034
1048
  * Widget definitions array. Each widget defines metadata for rendering.
1035
1049
  *
@@ -3602,6 +3616,12 @@ export default {
3602
3616
 
3603
3617
  getWidgetButtons(item) {
3604
3618
  const def = this.getWidgetDef(item.widgetId)
3619
+ // `showButtons: false` on the placement or the definition drops
3620
+ // the footer links ("More events"), for a placement whose card
3621
+ // says enough without them (zuiddrecht-pixel-gaps-3).
3622
+ if (item?.showButtons === false || def?.showButtons === false) {
3623
+ return []
3624
+ }
3605
3625
  return def?.buttons || []
3606
3626
  },
3607
3627
 
@@ -3905,11 +3925,16 @@ export default {
3905
3925
  // library's own defaults, so a manifest need not set them — but a
3906
3926
  // manifest that DOES set them must reach the component, or the
3907
3927
  // declaration is a silent no-op that reads like configuration.
3908
- for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass', 'format']) {
3928
+ for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass', 'format', 'layout']) {
3909
3929
  if (props[key] !== undefined) {
3910
3930
  out[key] = props[key]
3911
3931
  }
3912
3932
  }
3933
+ // `content.layout: "stacked"`, the key a stat tile takes, works on
3934
+ // a stats block too (zuiddrecht-pixel-gaps-3).
3935
+ if (out.layout === undefined && typeof content.layout === 'string') {
3936
+ out.layout = content.layout
3937
+ }
3913
3938
  // `countLabel` is the unit beside the number ("0 cases", "0 tasks").
3914
3939
  // It is manifest-authored prose and was being forwarded raw, so a
3915
3940
  // Dutch account read "0 cases" beside a translated widget title.
@@ -361,6 +361,40 @@ import { CnLockIndicator } from '../CnLockIndicator/index.js'
361
361
  // consuming app does not pull in the library's global css/index.css.
362
362
  import '../../css/table.css'
363
363
 
364
+ /**
365
+ * Fill a `secondary` template such as `"{identifier} · {requester}"`.
366
+ *
367
+ * A field without a value takes its separator with it: the text between two
368
+ * fields belongs to the field after it, so an empty requester leaves
369
+ * "2026-0002", not "2026-0002 ·", and an empty first field does not leave a
370
+ * leading separator either. Text before the first field and after the last
371
+ * one is kept as written. A template whose fields all have values fills
372
+ * exactly as before.
373
+ *
374
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it
375
+ * @param {string} template The template, with `{field}` placeholders.
376
+ * @param {(key: string) => string} valueOf The text of a field ('' when empty).
377
+ * @return {string} The filled template.
378
+ */
379
+ function fillSecondaryTemplate(template, valueOf) {
380
+ // split() with a capture group alternates literal, field, literal, …
381
+ const parts = template.split(/\{([^}]+)\}/)
382
+ const lead = parts[0]
383
+ const tail = parts[parts.length - 1]
384
+ let body = ''
385
+ for (let i = 1; i < parts.length; i += 2) {
386
+ const value = valueOf(parts[i].trim())
387
+ if (value === '') {
388
+ continue
389
+ }
390
+ // The literal before this field: the separator, unless nothing has
391
+ // been written yet (then there is nothing to separate from).
392
+ const separator = i === 1 ? '' : parts[i - 1]
393
+ body += (body === '' ? '' : separator) + value
394
+ }
395
+ return lead + body + tail
396
+ }
397
+
364
398
  /**
365
399
  * CnDataTable — Generic sortable data table for list views.
366
400
  *
@@ -1451,7 +1485,7 @@ export default {
1451
1485
  }
1452
1486
  const asText = (v) => (v === null || v === undefined ? '' : String(v))
1453
1487
  if (spec.includes('{')) {
1454
- const filled = spec.replace(/\{([^}]+)\}/g, (_, key) => asText(this.getCellValue(row, key.trim())))
1488
+ const filled = fillSecondaryTemplate(spec, (key) => asText(this.getCellValue(row, key)))
1455
1489
  // A template whose every field is empty leaves only separators:
1456
1490
  // nothing worth a line.
1457
1491
  return /[\p{L}\p{N}]/u.test(filled) ? filled.trim() : ''
@@ -444,3 +444,5 @@ When the schema-driven fetch answers 404 the page shows a not-found state whose
444
444
  ### Header card and header widget
445
445
 
446
446
  `headerCard` (default `false`) draws the header as a bordered card. `headerWidget` (default `''`) names a widget in `widgets` to render inside the header under the title, without a card of its own and out of the body grid: the stages bars of the board's case card.
447
+
448
+ `showWidgetActions` (Boolean, default `true`): `false` (manifest `config.showWidgetActions: false`) drops the overflow Actions menu from the cards of the body grid and the side column, unless a widget definition sets `showActions: true`.
@@ -48,6 +48,7 @@
48
48
  class="cn-detail-page__breadcrumbs"
49
49
  :crumbs="breadcrumbCrumbs"
50
50
  :rootText="breadcrumbCrumbs[0].icon === undefined"
51
+ :separator="breadcrumbSeparator"
51
52
  data-testid="cn-detail-page-breadcrumbs" />
52
53
  <!-- Header -->
53
54
  <div
@@ -749,6 +750,7 @@
749
750
  :widget="findWidget(item)"
750
751
  chrome="card"
751
752
  :showCardTitle="showCardTitle(item)"
753
+ :showActions="showWidgetActions"
752
754
  :objectId="objectId"
753
755
  :object="currentObject"
754
756
  :objectType="resolvedObjectType"
@@ -959,6 +961,7 @@
959
961
  :widget="widget"
960
962
  chrome="card"
961
963
  :showCardTitle="true"
964
+ :showActions="showWidgetActions"
962
965
  :objectId="objectId"
963
966
  :object="currentObject"
964
967
  :objectType="resolvedObjectType"
@@ -2235,18 +2238,38 @@ export default {
2235
2238
  * belongs to; the record's display name follows as the current
2236
2239
  * crumb. `label` goes through the host translate function and shows
2237
2240
  * as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,
2238
- * with the label as its accessible name. Null (the default) draws no
2241
+ * with the label as its accessible name. `currentField` names a field
2242
+ * of the record (a dotted path, e.g. `identifier`) whose value is the
2243
+ * current crumb instead of the display name; an empty value falls back
2244
+ * to the display name. `separator` draws that text (e.g. `/`) between
2245
+ * the crumbs instead of the chevron. Null (the default) draws no
2239
2246
  * trail.
2240
2247
  *
2241
2248
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
2242
2249
  * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
2243
- * @type {{label: string, route?: string, params?: object, href?: string, icon?: string}|null}
2250
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
2251
+ * @type {{label: string, route?: string, params?: object, href?: string, icon?: string, currentField?: string, separator?: string}|null}
2244
2252
  */
2245
2253
  breadcrumb: {
2246
2254
  type: Object,
2247
2255
  default: null,
2248
2256
  },
2249
2257
 
2258
+ /**
2259
+ * Whether the record's widget cards (the body grid and the side
2260
+ * column) keep their overflow Actions menu when their definition does
2261
+ * not say (`showActions`). `false` (manifest
2262
+ * `config.showWidgetActions: false`) drops it, as on a dashboard; a
2263
+ * widget with `showActions: true` keeps its menu. A card that offers
2264
+ * an Add action keeps the menu that holds it. True by default.
2265
+ *
2266
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-detail-page-can-drop-the-widget-actions-menu
2267
+ */
2268
+ showWidgetActions: {
2269
+ type: Boolean,
2270
+ default: true,
2271
+ },
2272
+
2250
2273
  /**
2251
2274
  * A column of cards beside the body (manifest `config.sideColumn`),
2252
2275
  * for the record's facts: deadline, requester, handler, links. Each
@@ -3438,7 +3461,38 @@ export default {
3438
3461
  } else if (crumb.href) {
3439
3462
  first.href = crumb.href
3440
3463
  }
3441
- return [first, { label: this.displayTitle }]
3464
+ return [first, { label: this.breadcrumbCurrentLabel }]
3465
+ },
3466
+
3467
+ /**
3468
+ * The current crumb: the record's `breadcrumb.currentField` value
3469
+ * (the case number on a case page) when declared and filled, else the
3470
+ * display name.
3471
+ *
3472
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
3473
+ * @return {string}
3474
+ */
3475
+ breadcrumbCurrentLabel() {
3476
+ const field = this.breadcrumb && this.breadcrumb.currentField
3477
+ if (typeof field === 'string' && field !== '') {
3478
+ const value = stageOf(this.resolvedObject, field)
3479
+ if (value !== '') {
3480
+ return value
3481
+ }
3482
+ }
3483
+ return this.displayTitle
3484
+ },
3485
+
3486
+ /**
3487
+ * The text drawn between the crumbs (`breadcrumb.separator`), or ''
3488
+ * for the chevron.
3489
+ *
3490
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
3491
+ * @return {string}
3492
+ */
3493
+ breadcrumbSeparator() {
3494
+ const separator = this.breadcrumb && this.breadcrumb.separator
3495
+ return typeof separator === 'string' ? separator : ''
3442
3496
  },
3443
3497
 
3444
3498
  /**
@@ -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;