@conduction/nextcloud-vue 2.69.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 (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
@@ -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
  }
@@ -111,6 +111,15 @@
111
111
  data-testid="cn-stat-widget-caption">
112
112
  {{ resolvedCaption }}
113
113
  </span>
114
+ <!-- A second caption line (`content.note`), as LpStart's "3
115
+ deadlines this week" under the count. Static text for now. -->
116
+ <span
117
+ v-if="!displayLoading && !displayError && resolvedNote"
118
+ class="cn-kpi-card__label cn-stat-widget__note cn-kpi-card__caption-line"
119
+ :class="noteVariantClass"
120
+ data-testid="cn-stat-widget-note">
121
+ {{ resolvedNote }}
122
+ </span>
114
123
  </div>
115
124
  </component>
116
125
  </template>
@@ -663,6 +672,33 @@ export default {
663
672
  return rules.find((r) => r && this.matchesRule(current, r)) || null
664
673
  },
665
674
 
675
+ /**
676
+ * The second caption line: `content.note` through the host translate
677
+ * function, or '' without it.
678
+ *
679
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stat-tile-can-carry-a-second-caption-line
680
+ * @return {string}
681
+ */
682
+ resolvedNote() {
683
+ const note = this.content && this.content.note
684
+ return typeof note === 'string' && note !== '' ? this.effectiveTranslate(note) : ''
685
+ },
686
+
687
+ /**
688
+ * The colour class of the second caption line (`content.noteVariant`,
689
+ * the same names as `captionVariant`), or '' for the muted default.
690
+ *
691
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stat-tile-can-carry-a-second-caption-line
692
+ * @return {string}
693
+ */
694
+ noteVariantClass() {
695
+ const variant = this.content && this.content.noteVariant
696
+ if (!variant || variant === 'default' || !Object.hasOwn(VARIANT_COLORS, variant)) {
697
+ return ''
698
+ }
699
+ return `cn-kpi-card__label--${variant === 'danger' ? 'error' : variant}`
700
+ },
701
+
666
702
  /**
667
703
  * The caption's colour class: the matching override's
668
704
  * `captionVariant`, else `content.captionVariant`, as
@@ -151,3 +151,5 @@ export default {
151
151
  | Slot | Scope | Description |
152
152
  |---|---|---|
153
153
  | `value` | `{ count, formatted }` | Override the prominently-displayed value — render a pre-formatted string (currency, percent, a "—" placeholder, …). `count` is the raw `count` prop; `formatted` is the locale-formatted string the default slot renders. Useful for currency / unit prefixes (e.g. `€{{ formatted }}`). |
154
+
155
+ `layout` (String, default empty): `stacked` is the board's tile, as CnStatWidget `content.layout: "stacked"`: no icon circle, a 14px muted title, the number at 34px/700 and the unit on a muted line under it.
@@ -19,7 +19,7 @@
19
19
  v-bind="componentAttrs"
20
20
  @click="onClick">
21
21
  <!-- Icon -->
22
- <div v-if="hasIcon" class="cn-kpi-card__icon cn-stats-block__icon" :class="iconClasses">
22
+ <div v-if="hasIcon && !isStacked" class="cn-kpi-card__icon cn-stats-block__icon" :class="iconClasses">
23
23
  <slot name="icon">
24
24
  <component :is="icon" v-if="icon" :size="iconSize" />
25
25
  </slot>
@@ -249,6 +249,18 @@ export default {
249
249
  default: false,
250
250
  },
251
251
 
252
+ /**
253
+ * `stacked` is the board's tile, as CnStatWidget `content.layout:
254
+ * "stacked"`: no icon circle, a 14px muted title, the number at
255
+ * 34px/700 in the text colour. Empty (the default) keeps the KPI card.
256
+ *
257
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look
258
+ */
259
+ layout: {
260
+ type: String,
261
+ default: '',
262
+ },
263
+
252
264
  /**
253
265
  * Lay the icon left of the content. No longer needed — this is the
254
266
  * canonical card's own layout — and kept so existing callers that pass
@@ -366,13 +378,24 @@ export default {
366
378
  return {}
367
379
  },
368
380
 
381
+ /**
382
+ * Whether the block takes the stacked board look (`layout`).
383
+ *
384
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look
385
+ * @return {boolean}
386
+ */
387
+ isStacked() {
388
+ return this.layout === 'stacked'
389
+ },
390
+
369
391
  rootClasses() {
370
392
  return {
371
393
  // Canonical + legacy, in pairs: the canonical class carries the
372
394
  // look, the legacy one keeps existing app CSS matching.
373
395
  // The canonical card is already horizontal, so the legacy pair
374
396
  // is emitted for app CSS but carries no look of its own.
375
- 'cn-kpi-card--horizontal': this.horizontal || !this.vertical,
397
+ 'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),
398
+ 'cn-kpi-card--stacked': this.isStacked,
376
399
  'cn-stats-block--horizontal': this.horizontal || !this.vertical,
377
400
  'cn-kpi-card--vertical': this.vertical,
378
401
  'cn-stats-block--vertical': this.vertical,
@@ -32,6 +32,7 @@
32
32
  :showZeroCount="showZeroCount"
33
33
  :horizontal="horizontal"
34
34
  :vertical="vertical"
35
+ :layout="layout"
35
36
  :filled="filled"
36
37
  :clickable="!!view.route"
37
38
  :route="view.route || null">
@@ -54,6 +55,7 @@
54
55
  :showZeroCount="showZeroCount"
55
56
  :horizontal="horizontal"
56
57
  :vertical="vertical"
58
+ :layout="layout"
57
59
  :filled="filled"
58
60
  :clickable="!!route"
59
61
  :route="route">
@@ -260,6 +262,17 @@ export default {
260
262
  default: false,
261
263
  },
262
264
 
265
+ /**
266
+ * `stacked` gives every block the board look (see CnStatsBlock
267
+ * `layout`). Empty (the default) keeps the KPI card.
268
+ *
269
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look
270
+ */
271
+ layout: {
272
+ type: String,
273
+ default: '',
274
+ },
275
+
263
276
  /**
264
277
  * Draw each tile's own grey box. Off by default — the widget is
265
278
  * rendered inside a CnWidgetWrapper that already draws a card.
@@ -3,7 +3,7 @@
3
3
  SPDX-License-Identifier: EUPL-1.2
4
4
  -->
5
5
  <template>
6
- <div class="cn-week-strip" data-testid="cn-week-strip">
6
+ <div class="cn-week-strip" :class="{ 'cn-week-strip--inset': inset }" data-testid="cn-week-strip">
7
7
  <div v-if="loading" class="cn-week-strip__state" role="status">
8
8
  <NcLoadingIcon :size="20" />
9
9
  <span class="cn-week-strip__state-text">{{ loadingLabel }}</span>
@@ -118,9 +118,11 @@ export default {
118
118
  * today), `lateField` a boolean field that marks a record late.
119
119
  * `items` are static entries `{ title, meta?, date, route?, href?, late? }`.
120
120
  * `emptyText` is the text of a day without items. `weekOffset` moves
121
- * the strip whole weeks from the current one.
121
+ * the strip whole weeks from the current one. `inset: true` draws the
122
+ * strip inside the board's inset (16px above, 24px at the sides, 22px
123
+ * below) instead of from card edge to card edge.
122
124
  *
123
- * @type {{days?: (5|7), source?: {register?: string, schema?: string, filter?: object, limit?: number}, dateField?: string, titleField?: string, metaFields?: Array<string>, itemRoute?: string, lateWhen?: {op?: string, value?: number}, lateField?: string, items?: Array<{title: string, meta?: string, date: string, route?: (string|object), href?: string, late?: boolean}>, emptyText?: string, weekOffset?: number}}
125
+ * @type {{days?: (5|7), inset?: boolean, source?: {register?: string, schema?: string, filter?: object, limit?: number}, dateField?: string, titleField?: string, metaFields?: Array<string>, itemRoute?: string, lateWhen?: {op?: string, value?: number}, lateField?: string, items?: Array<{title: string, meta?: string, date: string, route?: (string|object), href?: string, late?: boolean}>, emptyText?: string, weekOffset?: number}}
124
126
  */
125
127
  content: {
126
128
  type: Object,
@@ -159,6 +161,17 @@ export default {
159
161
  },
160
162
 
161
163
  computed: {
164
+ /**
165
+ * Whether the widget draws the board's inset (`content.inset`). Off
166
+ * by default, which keeps it edge to edge as before.
167
+ *
168
+ * @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
169
+ * @return {boolean}
170
+ */
171
+ inset() {
172
+ return Boolean(this.content && this.content.inset === true)
173
+ },
174
+
162
175
  /**
163
176
  * The translate function in use.
164
177
  *
@@ -542,6 +555,14 @@ export default {
542
555
  width: 100%;
543
556
  }
544
557
 
558
+ /* `content.inset: true`: the board's inset inside the card (zuiddrecht-pixel-
559
+ gaps-3). A dashboard renders its widgets flush, so without it the strip ran
560
+ from card edge to card edge. Theme hook: --cn-widget-board-inset. */
561
+ .cn-week-strip--inset {
562
+ box-sizing: border-box;
563
+ padding: var(--cn-widget-board-inset, 16px 24px 22px);
564
+ }
565
+
545
566
  .cn-week-strip__state {
546
567
  display: flex;
547
568
  align-items: center;
@@ -46,6 +46,16 @@
46
46
  row-gap: var(--cn-detail-header-row-gap, 22px);
47
47
  }
48
48
 
49
+ /* That wrap must not take the actions off the title row. With `flex-basis:
50
+ auto` a long title claimed its whole text width, so the actions wrapped
51
+ under it. From a zero basis the title takes what the actions leave and
52
+ wraps (two lines, then an ellipsis) inside it; only on a page too narrow
53
+ for 16rem of title beside the actions do the actions drop a row. */
54
+ .cn-detail-page__header--with-widget > :first-child {
55
+ flex: 1 1 0;
56
+ min-width: min(100%, 16rem);
57
+ }
58
+
49
59
  .cn-detail-page__header-widget {
50
60
  flex: 1 0 100%;
51
61
  min-width: 0;
@@ -10,6 +10,18 @@
10
10
  padding: calc(5 * var(--default-grid-baseline));
11
11
  }
12
12
 
13
+ /* The board's header buttons (`headerButtons`) at the end of the title row,
14
+ centred on the title and its count line. They wrap under the title on a
15
+ narrow page. */
16
+ .cn-index-page__header-buttons {
17
+ display: flex;
18
+ flex-wrap: wrap;
19
+ align-items: center;
20
+ align-self: center;
21
+ gap: calc(2 * var(--default-grid-baseline));
22
+ margin-inline-start: auto;
23
+ }
24
+
13
25
  .cn-index-page__below-header {
14
26
  margin-bottom: calc(4 * var(--default-grid-baseline));
15
27
  }
@@ -414,6 +414,21 @@ a.cn-kpi-card--stacked:focus-visible {
414
414
  text-decoration: none !important;
415
415
  }
416
416
 
417
+ /* A stacked stats block (CnStatsBlock `layout: "stacked"`): its unit
418
+ ("decisions") goes on a muted line of its own under the number, where the
419
+ stat tile puts its caption (zuiddrecht-pixel-gaps-3). */
420
+ .cn-kpi-card--stacked .cn-stats-block__count {
421
+ flex-direction: column;
422
+ align-items: flex-start;
423
+ gap: var(--cn-kpi-stacked-gap, 6px);
424
+ }
425
+
426
+ .cn-kpi-card--stacked .cn-stats-block__count-label {
427
+ font-size: var(--cn-kpi-label-size, 13px);
428
+ font-weight: 500;
429
+ color: var(--color-text-maxcontrast);
430
+ }
431
+
417
432
  /* Filled: the card draws its own grey box, for a KPI that is NOT already
418
433
  inside a wrapper that draws one. Opt-in via `content.flat: false`. */
419
434
  .cn-kpi-card--filled {