@conduction/nextcloud-vue 2.56.0-beta.12 → 2.56.0-beta.13

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 (84) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +99 -3
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +23 -7
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
  6. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  7. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
  8. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +125 -4
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +439 -381
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  14. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  16. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +142 -26
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +195 -124
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  22. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  23. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  24. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  26. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +84 -11
  27. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +3 -2
  29. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  31. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  32. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  33. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  34. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
  37. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
  39. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  40. package/dist/esm/l10n/en.json.js +3 -0
  41. package/dist/esm/l10n/en.json.js.map +1 -1
  42. package/dist/esm/l10n/nl.json.js +3 -0
  43. package/dist/esm/l10n/nl.json.js.map +1 -1
  44. package/dist/esm/mixins/pageViews.js +400 -0
  45. package/dist/esm/mixins/pageViews.js.map +1 -0
  46. package/dist/esm/nextcloud-vue.css +182 -0
  47. package/dist/esm/store/useObjectStore.js +118 -33
  48. package/dist/esm/store/useObjectStore.js.map +1 -1
  49. package/dist/esm/utils/actionsDispatcher.js +25 -2
  50. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  51. package/dist/esm/utils/columnFilters.js +27 -3
  52. package/dist/esm/utils/columnFilters.js.map +1 -1
  53. package/dist/esm/utils/schema.js +40 -2
  54. package/dist/esm/utils/schema.js.map +1 -1
  55. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  56. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  57. package/dist/nextcloud-vue.cjs.js +2001 -684
  58. package/dist/nextcloud-vue.cjs.js.map +2 -2
  59. package/dist/nextcloud-vue.css +182 -0
  60. package/l10n/en.json +3 -0
  61. package/l10n/nl.json +3 -0
  62. package/package.json +1 -1
  63. package/src/components/CnAppNav/CnAppNav.vue +148 -7
  64. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
  65. package/src/components/CnDashboardPage/CnDashboardPage.vue +194 -10
  66. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  67. package/src/components/CnDetailPage/CnDetailPage.md +5 -1
  68. package/src/components/CnDetailPage/CnDetailPage.vue +208 -11
  69. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  70. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  71. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +100 -13
  72. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  73. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  74. package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
  75. package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
  76. package/src/css/detail-page.css +27 -0
  77. package/src/css/kpi-card.css +47 -0
  78. package/src/mixins/pageViews.js +396 -0
  79. package/src/schemas/app-manifest-v2.schema.json +51 -2
  80. package/src/store/useObjectStore.js +118 -33
  81. package/src/utils/actionsDispatcher.js +25 -2
  82. package/src/utils/columnFilters.js +29 -2
  83. package/src/utils/schema.js +40 -2
  84. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -32,6 +32,18 @@
32
32
  </p>
33
33
  </div>
34
34
  <div class="cn-dashboard-page__header-actions">
35
+ <!-- The page's view switch (`views`), unless a greeting header
36
+ widget on the page draws it. Its options control the view
37
+ region below the grid. -->
38
+ <CnSegmentedControl
39
+ v-if="showsPageViewSwitch"
40
+ class="cn-page-view-switch"
41
+ :options="viewSwitchOptions"
42
+ :modelValue="activeViewId"
43
+ :ariaLabel="viewSwitchLabel"
44
+ :controls="viewRegionId"
45
+ data-testid="cn-dashboard-page-view-switch"
46
+ @update:modelValue="selectView" />
35
47
  <!-- Declarative header actions (#91 Wave 3): a manifest
36
48
  `headerActions[]` renders as buttons (open-form / api-call /
37
49
  toggle / navigate / refresh) with visibleWhen gating,
@@ -218,7 +230,7 @@
218
230
  sections (before-grid / after-grid / end): a page can legitimately
219
231
  have no grid widgets yet still show bodyWidget content, so
220
232
  "No widgets configured" would be a false negative. -->
221
- <div v-else-if="!hasWidgets && !hasBodyWidgets" class="cn-dashboard-page__empty">
233
+ <div v-else-if="!hasWidgets && !hasBodyWidgets && !hasViews" class="cn-dashboard-page__empty">
222
234
  <!-- @slot empty Replaces the default empty state shown when the dashboard has no widgets. Defaults to an `NcEmptyContent` block. -->
223
235
  <slot name="empty">
224
236
  <NcEmptyContent :description="emptyLabel">
@@ -253,15 +265,34 @@
253
265
  to the layout/widgets arrays — in-place pushes (Add widget) keep
254
266
  the same array identity and can't re-render it. One remount per
255
267
  edit flip re-subscribes against the now-reactive graph. -->
268
+ <!-- The view switch for a page without a header row, when no greeting
269
+ header widget draws it. -->
270
+ <div
271
+ v-if="gridSectionsShown && !showHeader && showsPageViewSwitch"
272
+ class="cn-dashboard-page__view-switch">
273
+ <CnSegmentedControl
274
+ class="cn-page-view-switch"
275
+ :options="viewSwitchOptions"
276
+ :modelValue="activeViewId"
277
+ :ariaLabel="viewSwitchLabel"
278
+ :controls="viewRegionId"
279
+ data-testid="cn-dashboard-page-view-switch"
280
+ @update:modelValue="selectView" />
281
+ </div>
282
+ <!-- One grid per section: the page's own layout, then the chosen
283
+ view (`views`) in a region the view switch controls. Both are the
284
+ same grid with the same widget rendering below. A view grid is
285
+ keyed on its view, so switching mounts the other view's grid. -->
256
286
  <CnDashboardGrid
257
- v-else
258
- :key="`cn-dashboard-grid-${gridEditable ? 'editing' : 'live'}`"
259
- :layout="displayLayout"
260
- :editable="gridEditable"
287
+ v-for="section in gridSections"
288
+ :key="`cn-dashboard-grid-${section.key}-${gridEditable ? 'editing' : 'live'}`"
289
+ v-bind="section.attrs"
290
+ :layout="section.layout"
291
+ :editable="section.editable"
261
292
  :columns="columns"
262
293
  :cellHeight="cellHeight"
263
294
  :margin="gridMargin"
264
- @layoutChange="onLayoutChange">
295
+ @layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
265
296
  <template #widget="{ item }">
266
297
  <!-- In-app edit overlay (ADR-041): a single launchpad-style
267
298
  configure cog opens the per-widget style/config editor,
@@ -404,6 +435,7 @@
404
435
  :iconUrl="getWidgetIconUrl(item)"
405
436
  :iconClass="getWidgetIconClass(item)"
406
437
  :showTitle="widgetShowTitle(item)"
438
+ :showActions="widgetShowActions(item)"
407
439
  :borderless="widgetBorderless(item)"
408
440
  :flush="item.flush !== false"
409
441
  :buttons="getWidgetButtons(item)"
@@ -508,6 +540,7 @@
508
540
  :iconUrl="getWidgetIconUrl(item)"
509
541
  :iconClass="getWidgetIconClass(item)"
510
542
  :showTitle="widgetShowTitle(item)"
543
+ :showActions="widgetShowActions(item)"
511
544
  :borderless="widgetBorderless(item)"
512
545
  :flush="item.flush !== false"
513
546
  :buttons="getWidgetButtons(item)"
@@ -546,6 +579,7 @@
546
579
  :iconUrl="getWidgetIconUrl(item)"
547
580
  :iconClass="getWidgetIconClass(item)"
548
581
  :showTitle="widgetShowTitle(item)"
582
+ :showActions="widgetShowActions(item)"
549
583
  :buttons="getWidgetButtons(item)"
550
584
  :headerLink="getWidgetHeaderLink(item)"
551
585
  :styleConfig="item.styleConfig || {}"
@@ -652,6 +686,22 @@
652
686
  </template>
653
687
  </CnDashboardGrid>
654
688
 
689
+ <!-- The chosen view has nothing to draw: say so in the region, never
690
+ leave a blank area under the switch. -->
691
+ <div
692
+ v-if="gridSectionsShown && viewIsEmpty"
693
+ :id="viewRegionId"
694
+ role="region"
695
+ :aria-label="activeView ? activeView.label : null"
696
+ class="cn-page-view-region cn-page-view-region--empty"
697
+ data-testid="cn-dashboard-page-view-empty">
698
+ <NcEmptyContent :description="viewEmptyText">
699
+ <template #icon>
700
+ <ViewDashboardOutline :size="48" />
701
+ </template>
702
+ </NcEmptyContent>
703
+ </div>
704
+
655
705
  <!-- Declarative in-body sections, `placement: "after-grid"` — host-app
656
706
  section components rendered BELOW the widget grid. -->
657
707
  <CnBodySections
@@ -711,6 +761,7 @@ import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
711
761
  import CnChartWidget from '../CnChartWidget/CnChartWidget.vue'
712
762
  import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
713
763
  import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow } from '../CnDateRangePicker/CnDateRangePicker.vue'
764
+ import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
714
765
  import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
715
766
  import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
716
767
  import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
@@ -718,6 +769,7 @@ import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
718
769
  import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
719
770
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
720
771
  import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
772
+ import { pageViews } from '../../mixins/pageViews.js'
721
773
  import { dashboardLayoutKey, mergeUserLayout } from '../../store/plugins/dashboardLayouts.js'
722
774
  import { isAppInstalled } from '../../utils/appInstalled.js'
723
775
  import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
@@ -872,8 +924,11 @@ export default {
872
924
  CnBuildiqEditButton,
873
925
  CnWidgetStyleEditorModal,
874
926
  CnLeafMountHost,
927
+ CnSegmentedControl,
875
928
  },
876
929
 
930
+ mixins: [pageViews],
931
+
877
932
  inject: {
878
933
  /**
879
934
  * Buildiq in-app edit state (ADR-041), provided by CnAppRoot as a ref.
@@ -946,6 +1001,21 @@ export default {
946
1001
  default: true,
947
1002
  },
948
1003
 
1004
+ /**
1005
+ * Whether a widget draws its overflow Actions menu when neither its
1006
+ * placement nor its definition says (`showActions`). `false` (manifest
1007
+ * `config.showWidgetActions: false`) drops the menu from every such
1008
+ * widget, for a dashboard whose widgets carry a header link and
1009
+ * nothing else. A widget that sets `showActions: true` keeps its menu.
1010
+ * Card widgets have no menu either way.
1011
+ *
1012
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-dashboard-can-drop-the-widget-actions-menu
1013
+ */
1014
+ showWidgetActions: {
1015
+ type: Boolean,
1016
+ default: true,
1017
+ },
1018
+
949
1019
  /**
950
1020
  * Widget definitions array. Each widget defines metadata for rendering.
951
1021
  *
@@ -1861,6 +1931,89 @@ export default {
1861
1931
  return this.layout.length > 0 || this.widgetRefItems.length > 0
1862
1932
  },
1863
1933
 
1934
+ /**
1935
+ * Whether the grid sections render at all: not while loading, not
1936
+ * while the widget predicates settle, not on the empty page and not
1937
+ * on a widget-ref content page.
1938
+ *
1939
+ * @return {boolean}
1940
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
1941
+ */
1942
+ gridSectionsShown() {
1943
+ if (this.loading || (!this.widgetConditionsSettled && !this.gridEditable)) {
1944
+ return false
1945
+ }
1946
+ if (!this.hasWidgets && !this.hasBodyWidgets && !this.hasViews) {
1947
+ return false
1948
+ }
1949
+ return this.widgetRefItems.length === 0
1950
+ },
1951
+
1952
+ /**
1953
+ * The chosen view's layout as the grid draws it: in live mode the
1954
+ * widgets that would draw nothing leave and the rest close up, as
1955
+ * for the page's own layout.
1956
+ *
1957
+ * @return {Array<object>}
1958
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
1959
+ */
1960
+ viewDisplayLayout() {
1961
+ const items = this.activeView ? this.activeView.layout : []
1962
+ if (this.gridEditable) {
1963
+ return items
1964
+ }
1965
+ const visible = items.filter((item) => !this.isCollapsedWidget(item))
1966
+ return visible.length === items.length ? items : this.compactDisplayLayout(visible)
1967
+ },
1968
+
1969
+ /**
1970
+ * Whether the chosen view has nothing to draw.
1971
+ *
1972
+ * @return {boolean}
1973
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
1974
+ */
1975
+ viewIsEmpty() {
1976
+ return this.hasViews && this.viewDisplayLayout.length === 0
1977
+ },
1978
+
1979
+ /**
1980
+ * The grids this page draws. Without views: the page's own layout,
1981
+ * as it always was. With views: the page's own layout when it has
1982
+ * items, then the chosen view's layout in the region the switch
1983
+ * controls (left out when the view is empty; the empty state takes
1984
+ * the region then).
1985
+ *
1986
+ * @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}
1987
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
1988
+ */
1989
+ gridSections() {
1990
+ if (!this.gridSectionsShown) {
1991
+ return []
1992
+ }
1993
+ const page = { key: 'page', view: false, layout: this.displayLayout, editable: this.gridEditable, attrs: {} }
1994
+ if (!this.hasViews) {
1995
+ return [page]
1996
+ }
1997
+ const sections = this.displayLayout.length > 0 ? [page] : []
1998
+ if (!this.viewIsEmpty) {
1999
+ sections.push({
2000
+ key: `view-${this.activeViewId}`,
2001
+ view: true,
2002
+ layout: this.viewDisplayLayout,
2003
+ // A user arrangement covers the page's own layout only.
2004
+ editable: this.gridEditable && !this.userLayout,
2005
+ attrs: {
2006
+ id: this.viewRegionId,
2007
+ role: 'region',
2008
+ 'aria-label': this.activeView.label,
2009
+ class: 'cn-page-view-region',
2010
+ 'data-testid': 'cn-dashboard-page-view-region',
2011
+ },
2012
+ })
2013
+ }
2014
+ return sections
2015
+ },
2016
+
1864
2017
  /**
1865
2018
  * The widget settings object handed to CnWidgetStyleEditorModal,
1866
2019
  * built from the widget definition currently being configured
@@ -1991,6 +2144,14 @@ export default {
1991
2144
  this.evaluateWidgetConditions()
1992
2145
  },
1993
2146
  },
2147
+
2148
+ /** A view's widgets carry predicates too. */
2149
+ views: {
2150
+ deep: true,
2151
+ handler() {
2152
+ this.evaluateWidgetConditions()
2153
+ },
2154
+ },
1994
2155
  },
1995
2156
 
1996
2157
  created() {
@@ -2855,7 +3016,8 @@ export default {
2855
3016
  * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2856
3017
  */
2857
3018
  async evaluateWidgetConditions() {
2858
- const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
3019
+ const defs = [...(this.widgets || []), ...this.normalizedViews.flatMap((view) => view.widgets)]
3020
+ const conditional = defs.filter((def) => def && def.id && def.type
2859
3021
  && this.widgetDisplayConfig(def).visibleWhen)
2860
3022
  // Skip when the CONDITIONAL SET is unchanged: the deep `widgets`
2861
3023
  // watch fires on any def edit, but only an added/removed/edited
@@ -2989,7 +3151,7 @@ export default {
2989
3151
  */
2990
3152
  getWidgetDef(widgetId) {
2991
3153
  const list = Array.isArray(this.widgets) ? this.widgets : []
2992
- return list.find((w) => w && w.id === widgetId) || null
3154
+ return list.find((w) => w && w.id === widgetId) || this.findViewWidget(widgetId)
2993
3155
  },
2994
3156
 
2995
3157
  /**
@@ -3380,11 +3542,20 @@ export default {
3380
3542
  * exactly that reason; there is now one rule for every family, so a
3381
3543
  * custom slot and a registered widget cannot disagree about it.
3382
3544
  *
3545
+ * A greeting header that declares `content.ground: true` sits on the
3546
+ * page ground, so it asks for no card either: one key on the widget
3547
+ * instead of a second one on its placement.
3548
+ *
3549
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
3383
3550
  * @param {object} item the layout placement.
3384
3551
  * @return {boolean}
3385
3552
  */
3386
3553
  widgetBorderless(item) {
3387
- return item.borderless === true
3554
+ if (item.borderless === true) {
3555
+ return true
3556
+ }
3557
+ const def = this.getWidgetDef(item.widgetId)
3558
+ return Boolean(def && def.type === 'header' && this.getWidgetContent(item).ground === true)
3388
3559
  },
3389
3560
 
3390
3561
  /**
@@ -3401,7 +3572,7 @@ export default {
3401
3572
  const def = this.getWidgetDef(item.widgetId)
3402
3573
  const value = item.showActions !== undefined ? item.showActions : def?.showActions
3403
3574
  if (value === undefined || value === null) {
3404
- return !this.isCardWidget(item)
3575
+ return this.showWidgetActions && !this.isCardWidget(item)
3405
3576
  }
3406
3577
  return value !== false
3407
3578
  },
@@ -3994,6 +4165,19 @@ export default {
3994
4165
  padding: 60px 20px;
3995
4166
  }
3996
4167
 
4168
+ /* The page's view switch on a page without a header row: at the end of its
4169
+ own row, above the grids, where the header would have put it. */
4170
+ .cn-dashboard-page__view-switch {
4171
+ display: flex;
4172
+ justify-content: flex-end;
4173
+ margin-bottom: 12px;
4174
+ }
4175
+
4176
+ /* A view the switch shows with nothing to draw. */
4177
+ .cn-page-view-region--empty {
4178
+ padding: 40px 20px;
4179
+ }
4180
+
3997
4181
  .cn-dashboard-page__page-filters {
3998
4182
  display: flex;
3999
4183
  flex-wrap: wrap;
@@ -91,9 +91,9 @@
91
91
  </label>
92
92
  </div>
93
93
 
94
- <!-- string: equals -->
94
+ <!-- string: contains (OpenRegister `[like]`, case-insensitive) -->
95
95
  <label v-else class="cn-column-filter__field">
96
- <span class="cn-column-filter__label">{{ equalsLabel }}</span>
96
+ <span class="cn-column-filter__label">{{ containsLabel }}</span>
97
97
  <input
98
98
  v-model="draft.value"
99
99
  class="cn-column-filter__input"
@@ -131,7 +131,7 @@ let popoverSeq = 0
131
131
  *
132
132
  * Renders the control that fits the column: a checkbox list for an enum, yes,
133
133
  * no or any for a boolean, from and to for a number or a date, a searchable
134
- * list of referenced objects for a reference, and an equals box for text.
134
+ * list of referenced objects for a reference, and a contains box for text.
135
135
  * Real inputs throughout, so every control works from the keyboard. Escape
136
136
  * closes it and the host returns focus to the filter button.
137
137
  *
@@ -265,8 +265,14 @@ export default {
265
265
  return t('nextcloud-vue', 'To')
266
266
  },
267
267
 
268
- equalsLabel() {
269
- return t('nextcloud-vue', 'Equals')
268
+ /**
269
+ * Label of the text box: the text filter matches on contains.
270
+ *
271
+ * @return {string}
272
+ * @spec openspec/changes/header-filter-contains/specs/cn-data-table/spec.md#requirement-a-text-header-filter-matches-on-contains
273
+ */
274
+ containsLabel() {
275
+ return t('nextcloud-vue', 'Contains')
270
276
  },
271
277
 
272
278
  clearLabel() {
@@ -433,4 +433,8 @@ When the schema-driven fetch answers 404 the page shows a not-found state whose
433
433
 
434
434
  ### Type eyebrow and breadcrumb
435
435
 
436
- `showTypeEyebrow` (default `true`) can drop the type label above the record name. `breadcrumb` (`{ label, route?, params?, href? }`) draws a breadcrumb line above the header: that crumb, then the record's name as the current crumb.
436
+ `showTypeEyebrow` (default `true`) can drop the type label above the record name. `breadcrumb` (`{ label, route?, params?, href?, icon? }`) draws a breadcrumb line above the header: that crumb, then the record's name as the current crumb. The label shows as text; `icon` draws an icon instead.
437
+
438
+ ### Header card and header widget
439
+
440
+ `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.