@conduction/nextcloud-vue 2.64.0 → 2.65.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 (51) 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 +26 -2
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +6 -3
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +64 -5
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +66 -28
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  17. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +22 -5
  18. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +1 -1
  20. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  21. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
  24. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
  26. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  27. package/dist/esm/nextcloud-vue.css +164 -0
  28. package/dist/esm/store/useObjectStore.js +118 -33
  29. package/dist/esm/store/useObjectStore.js.map +1 -1
  30. package/dist/esm/utils/actionsDispatcher.js +14 -0
  31. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  32. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  33. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  34. package/dist/nextcloud-vue.cjs.js +488 -96
  35. package/dist/nextcloud-vue.cjs.js.map +2 -2
  36. package/dist/nextcloud-vue.css +164 -0
  37. package/package.json +1 -1
  38. package/src/components/CnAppNav/CnAppNav.vue +148 -7
  39. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
  40. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -2
  41. package/src/components/CnDetailPage/CnDetailPage.md +5 -1
  42. package/src/components/CnDetailPage/CnDetailPage.vue +114 -6
  43. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +34 -6
  44. package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
  45. package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
  46. package/src/css/detail-page.css +22 -0
  47. package/src/css/kpi-card.css +47 -0
  48. package/src/schemas/app-manifest-v2.schema.json +23 -2
  49. package/src/store/useObjectStore.js +118 -33
  50. package/src/utils/actionsDispatcher.js +14 -0
  51. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -47,9 +47,14 @@
47
47
  v-if="breadcrumbCrumbs.length > 0 && !objectNotFound"
48
48
  class="cn-detail-page__breadcrumbs"
49
49
  :crumbs="breadcrumbCrumbs"
50
+ :rootText="breadcrumbCrumbs[0].icon === undefined"
50
51
  data-testid="cn-detail-page-breadcrumbs" />
51
52
  <!-- Header -->
52
- <div v-if="!objectNotFound" class="cn-detail-page__header" data-testid="cn-detail-page-header">
53
+ <div
54
+ v-if="!objectNotFound"
55
+ class="cn-detail-page__header"
56
+ :class="{ 'cn-detail-page__header--card': headerCard, 'cn-detail-page__header--with-widget': headerWidgetDef !== null }"
57
+ data-testid="cn-detail-page-header">
53
58
  <!-- Header (left block) — overridable via #header slot. Default
54
59
  renders the icon + title + description. The right-hand
55
60
  #actions slot remains separate so headerComponent and
@@ -370,6 +375,50 @@
370
375
  </template>
371
376
  </CnActionsMenu>
372
377
  </div>
378
+ <!-- A widget inside the header (manifest `config.headerWidget`), on
379
+ a row of its own under the title and the actions: the stages
380
+ bars of the board's case card. Drawn without a card of its own,
381
+ and taken out of the body grid. Nothing renders without the
382
+ key. -->
383
+ <div
384
+ v-if="headerWidgetDef !== null"
385
+ class="cn-detail-page__header-widget"
386
+ data-testid="cn-detail-page-header-widget">
387
+ <!--
388
+ @slot `widget-${headerWidgetDef.id}`
389
+ @description The same per-widget slot the body grid and the side
390
+ column offer, for the widget placed in the header
391
+ (`headerWidget`). Same name and same bindings.
392
+ -->
393
+ <slot
394
+ :name="`widget-${headerWidgetDef.id}`"
395
+ :item="{ id: headerWidgetDef.id, widgetId: headerWidgetDef.id }"
396
+ :widget="headerWidgetDef"
397
+ :objectId="objectId"
398
+ :object="resolvedObject"
399
+ :objectData="resolvedObject"
400
+ :objectType="resolvedObjectType"
401
+ :register="register"
402
+ :schema="schema">
403
+ <CnDetailWidgetHost
404
+ :widget="headerWidgetDef"
405
+ chrome="bare"
406
+ :objectId="objectId"
407
+ :object="currentObject"
408
+ :objectType="resolvedObjectType"
409
+ :schemaObject="currentSchema"
410
+ :register="register"
411
+ :schema="schema"
412
+ :store="effectiveObjectStore"
413
+ :surface="surface"
414
+ :integrationContext="effectiveIntegrationContext"
415
+ :hideEmpty="hideEmpty"
416
+ :cnRegistry="cnRegistry"
417
+ :availableWidgets="widgets"
418
+ @geoSaved="onGeoSaved"
419
+ @openIntegration="onAutoBodyOpenIntegration" />
420
+ </slot>
421
+ </div>
373
422
  </div>
374
423
 
375
424
  <!-- The locked card, under the title, for ANY active lock — not only a
@@ -2104,11 +2153,14 @@ export default {
2104
2153
  * A breadcrumb line above the header (manifest `config.breadcrumb`):
2105
2154
  * `{ label, route?, params?, href? }` names the list the record
2106
2155
  * belongs to; the record's display name follows as the current
2107
- * crumb. `label` goes through the host translate function. Null (the
2108
- * default) draws no trail.
2156
+ * crumb. `label` goes through the host translate function and shows
2157
+ * as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,
2158
+ * with the label as its accessible name. Null (the default) draws no
2159
+ * trail.
2109
2160
  *
2110
2161
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
2111
- * @type {{label: string, route?: string, params?: object, href?: string}|null}
2162
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
2163
+ * @type {{label: string, route?: string, params?: object, href?: string, icon?: string}|null}
2112
2164
  */
2113
2165
  breadcrumb: {
2114
2166
  type: Object,
@@ -2130,6 +2182,33 @@ export default {
2130
2182
  default: () => [],
2131
2183
  },
2132
2184
 
2185
+ /**
2186
+ * Draw the header as a card (manifest `config.headerCard: true`): the
2187
+ * pills, the title and the actions on the page's surface colour inside
2188
+ * a bordered, rounded box, as the board's case header. Off by default,
2189
+ * which keeps the header on the page ground.
2190
+ *
2191
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
2192
+ */
2193
+ headerCard: {
2194
+ type: Boolean,
2195
+ default: false,
2196
+ },
2197
+
2198
+ /**
2199
+ * The id of a widget declared in `widgets` to render inside the header,
2200
+ * on its own row under the title and the actions (manifest
2201
+ * `config.headerWidget`), e.g. the stages bars. It renders without a
2202
+ * card of its own and leaves the body grid. Empty (the default), or an
2203
+ * id that names no widget, renders nothing extra.
2204
+ *
2205
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
2206
+ */
2207
+ headerWidget: {
2208
+ type: String,
2209
+ default: '',
2210
+ },
2211
+
2133
2212
  /**
2134
2213
  * Whether the current user administers this instance. Shows the
2135
2214
  * `adminOnly` header actions. `null` (the default) reads it from
@@ -2669,6 +2748,21 @@ export default {
2669
2748
  .filter(Boolean)
2670
2749
  },
2671
2750
 
2751
+ /**
2752
+ * The widget the header holds (`headerWidget`): the definition in
2753
+ * `widgets` with that id, or null without the key or a match.
2754
+ *
2755
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
2756
+ * @return {object|null} The widget definition, or null.
2757
+ */
2758
+ headerWidgetDef() {
2759
+ if (!this.headerWidget) {
2760
+ return null
2761
+ }
2762
+ const declared = Array.isArray(this.widgets) ? this.widgets : []
2763
+ return declared.find((widget) => widget && widget.id === this.headerWidget) || null
2764
+ },
2765
+
2672
2766
  /**
2673
2767
  * The side column's widget definitions. A string names a widget
2674
2768
  * declared in `widgets`; an object is a definition of its own.
@@ -3241,10 +3335,12 @@ export default {
3241
3335
  /**
3242
3336
  * The breadcrumb trail: the declared crumb (a router target from
3243
3337
  * `route` + `params`, or an `href`), then the record's display name
3244
- * as the current crumb. Empty without a `breadcrumb` label.
3338
+ * as the current crumb. Empty without a `breadcrumb` label. The first
3339
+ * crumb carries `icon` only when the manifest declares one.
3245
3340
  *
3246
3341
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
3247
- * @return {Array<{label: string, to?: object, href?: string}>}
3342
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
3343
+ * @return {Array<{label: string, to?: object, href?: string, icon?: string}>}
3248
3344
  */
3249
3345
  breadcrumbCrumbs() {
3250
3346
  const crumb = this.breadcrumb
@@ -3252,6 +3348,9 @@ export default {
3252
3348
  return []
3253
3349
  }
3254
3350
  const first = { label: this.effectiveTranslate(crumb.label) }
3351
+ if (typeof crumb.icon === 'string' && crumb.icon !== '') {
3352
+ first.icon = crumb.icon
3353
+ }
3255
3354
  if (crumb.route) {
3256
3355
  first.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
3257
3356
  } else if (crumb.href) {
@@ -4711,6 +4810,10 @@ export default {
4711
4810
  * it — CnPageRenderer's split pane forwards no slots, which is exactly
4712
4811
  * that fault and wants fixing, not concealing.
4713
4812
  *
4813
+ * The widget the header holds (`headerWidget`) draws nothing here
4814
+ * either: it is already drawn in the header.
4815
+ *
4816
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
4714
4817
  * @param {object} item A layout item.
4715
4818
  * @return {boolean} False only when the item provably draws nothing.
4716
4819
  */
@@ -4721,6 +4824,11 @@ export default {
4721
4824
  if (this.editingBody) {
4722
4825
  return true
4723
4826
  }
4827
+ // The widget the header holds (`headerWidget`) is drawn there, so
4828
+ // its grid cell would be the same widget twice.
4829
+ if (this.headerWidgetDef !== null && item.widgetId === this.headerWidgetDef.id) {
4830
+ return false
4831
+ }
4724
4832
  const widget = this.findWidget(item)
4725
4833
  if (!widget) {
4726
4834
  return true
@@ -4,7 +4,7 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, '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 }" :style="wrapperStyle">
8
8
  <div
9
9
  v-if="hasOverlay"
10
10
  class="cn-header-widget__overlay"
@@ -128,12 +128,15 @@ export default {
128
128
  * Persisted widget content: `{title, subtitle, backgroundImageUrl,
129
129
  * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
130
130
  * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
131
- * greeting, showDate, plain, views}`. All fields are optional except
131
+ * greeting, showDate, plain, ground, views}`. All fields are optional except
132
132
  * `title` (or `greeting`); unknown enum values collapse to documented
133
133
  * defaults and the renderer never throws. `views` is
134
134
  * `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented
135
135
  * control at the right of the heading whose checked option is the
136
- * current route; choosing another pushes its route.
136
+ * current route; choosing another pushes its route. `ground: true`
137
+ * draws the greeting on the page ground: the plain look with no
138
+ * padding, a 32px heading and the date line 6px above it (a dashboard
139
+ * also drops the widget's card for it).
137
140
  *
138
141
  * @type {object}
139
142
  */
@@ -269,7 +272,20 @@ export default {
269
272
  * @return {boolean}
270
273
  */
271
274
  isPlain() {
272
- return Boolean(this.content && this.content.plain === true)
275
+ return Boolean(this.content && (this.content.plain === true || this.content.ground === true))
276
+ },
277
+
278
+ /**
279
+ * Whether the greeting sits on the page ground (`content.ground`):
280
+ * the plain look without the card padding, so the date line and the
281
+ * heading align with the page edge as the board draws them. Off by
282
+ * default.
283
+ *
284
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
285
+ * @return {boolean}
286
+ */
287
+ isGround() {
288
+ return Boolean(this.content && this.content.ground === true)
273
289
  },
274
290
 
275
291
  /**
@@ -565,9 +581,10 @@ export default {
565
581
  'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],
566
582
  // Plain drops the coloured background, not the card padding:
567
583
  // at 0 the greeting sat flush against the card's left edge.
568
- padding: this.isPlain ? '16px' : '16px 24px',
584
+ // Ground has no card, so it has no padding either.
585
+ padding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),
569
586
  'box-sizing': 'border-box',
570
- gap: '8px',
587
+ gap: this.isGround ? '6px' : '8px',
571
588
  'text-align': this.textAlign,
572
589
  }
573
590
  },
@@ -765,6 +782,17 @@ export default {
765
782
  padding: 16px;
766
783
  }
767
784
 
785
+ /* On the page ground (`content.ground`): no card, so no inset around the
786
+ view switch, and the heading at the board's 32px. */
787
+ .cn-header-widget--ground .cn-header-widget__views {
788
+ padding: 0;
789
+ }
790
+
791
+ .cn-header-widget--ground .cn-header-widget__title {
792
+ font-size: var(--cn-header-ground-title-size, 32px);
793
+ letter-spacing: -0.01em;
794
+ }
795
+
768
796
  .cn-header-widget__date {
769
797
  font-size: 14px;
770
798
  line-height: 1.4;
@@ -1489,7 +1489,16 @@ export default {
1489
1489
  white-space: nowrap;
1490
1490
  }
1491
1491
 
1492
+ /* A clickable label is a <button>, and Nextcloud's core stylesheet gives
1493
+ every `button:not(.button-vue)` a min-height of the clickable area (34 to
1494
+ 44px) with the text centred in it. That pushed each reachable stage's label
1495
+ about 12px below the current one, which is a <span>. Zeroed here so all
1496
+ labels sit 8px under their bar, as the board draws them. The visually
1497
+ hidden "(current step)" is absolutely positioned and takes no space. */
1492
1498
  button.cn-stages-widget__bar-label {
1499
+ min-height: 0;
1500
+ height: auto;
1501
+ line-height: inherit;
1493
1502
  cursor: pointer;
1494
1503
  }
1495
1504
 
@@ -25,7 +25,7 @@
25
25
  ]"
26
26
  v-bind="linkAttrs">
27
27
  <div
28
- v-if="resolvedIcon"
28
+ v-if="resolvedIcon && !isStacked"
29
29
  class="cn-kpi-card__icon cn-stat-widget__icon"
30
30
  :style="iconCircleStyle">
31
31
  <CnWidgetIcon :name="resolvedIcon" :size="24" />
@@ -95,13 +95,22 @@
95
95
  </span>
96
96
  </template>
97
97
  <span
98
- v-if="!displayLoading && !displayError && resolvedCaption"
98
+ v-if="!isStacked && !displayLoading && !displayError && resolvedCaption"
99
99
  class="cn-kpi-card__label cn-stat-widget__caption"
100
100
  :class="captionVariantClass"
101
101
  data-testid="cn-stat-widget-caption">
102
102
  {{ resolvedCaption }}
103
103
  </span>
104
104
  </div>
105
+ <!-- Stacked (`content.layout: "stacked"`): the caption is a line of
106
+ its own under the number, as the board's "3 new this week". -->
107
+ <span
108
+ v-if="isStacked && !displayLoading && !displayError && resolvedCaption"
109
+ class="cn-kpi-card__label cn-stat-widget__caption cn-kpi-card__caption-line"
110
+ :class="captionVariantClass"
111
+ data-testid="cn-stat-widget-caption">
112
+ {{ resolvedCaption }}
113
+ </span>
105
114
  </div>
106
115
  </component>
107
116
  </template>
@@ -1242,12 +1251,31 @@ export default {
1242
1251
  /**
1243
1252
  * Card orientation. Horizontal (icon beside the number) is the
1244
1253
  * canonical KPI card; `content.layout: 'vertical'` stacks the icon
1245
- * above a centred number for a tile taller than it is wide.
1254
+ * above a centred number for a tile taller than it is wide;
1255
+ * `content.layout: 'stacked'` is the board look (see `isStacked`).
1246
1256
  *
1247
- * @return {'horizontal'|'vertical'}
1257
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-stat-tile-can-take-the-stacked-board-look
1258
+ * @return {'horizontal'|'vertical'|'stacked'}
1248
1259
  */
1249
1260
  cardLayout() {
1250
- return (this.content || {}).layout === 'vertical' ? 'vertical' : 'horizontal'
1261
+ const layout = (this.content || {}).layout
1262
+ if (layout === 'vertical' || layout === 'stacked') {
1263
+ return layout
1264
+ }
1265
+ return 'horizontal'
1266
+ },
1267
+
1268
+ /**
1269
+ * Whether the tile takes the stacked board look (`content.layout:
1270
+ * "stacked"`): a plain muted label, the number at 34px in the text
1271
+ * colour, the caption on a line of its own, and no icon circle. Off by
1272
+ * default.
1273
+ *
1274
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-stat-tile-can-take-the-stacked-board-look
1275
+ * @return {boolean}
1276
+ */
1277
+ isStacked() {
1278
+ return this.cardLayout === 'stacked'
1251
1279
  },
1252
1280
 
1253
1281
  /**
@@ -39,6 +39,28 @@
39
39
  min-width: 0;
40
40
  }
41
41
 
42
+ /* A widget in the header (`config.headerWidget`) takes a row of its own under
43
+ the title and the actions. */
44
+ .cn-detail-page__header--with-widget {
45
+ flex-wrap: wrap;
46
+ row-gap: var(--cn-detail-header-row-gap, 22px);
47
+ }
48
+
49
+ .cn-detail-page__header-widget {
50
+ flex: 1 0 100%;
51
+ min-width: 0;
52
+ }
53
+
54
+ /* The header as a card (`config.headerCard`): the board's case header, a
55
+ bordered box on the surface colour. The 56px inset that clears the
56
+ navigation toggle is not needed inside a card that starts below it. */
57
+ .cn-detail-page__header--card {
58
+ padding: var(--cn-detail-header-card-padding, 22px 24px);
59
+ border: 1px solid var(--color-border);
60
+ border-radius: var(--cn-detail-header-card-radius, 12px);
61
+ background: var(--color-main-background);
62
+ }
63
+
42
64
  .cn-detail-page__header-left {
43
65
  display: flex;
44
66
  align-items: center;
@@ -367,6 +367,53 @@
367
367
  font-size: var(--cn-kpi-value-size-stacked, 2rem);
368
368
  }
369
369
 
370
+ /* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
371
+ label, the number at 34px/700 in the text colour, and the caption on a line
372
+ of its own; CnStatWidget draws no icon circle in this layout. The padding
373
+ adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
374
+ Every value is a token so a theme can move it. */
375
+ .cn-kpi-card--stacked {
376
+ flex-direction: column;
377
+ align-items: stretch;
378
+ justify-content: center;
379
+ gap: 0;
380
+ padding: var(--cn-kpi-stacked-padding, 10px 6px);
381
+ }
382
+
383
+ .cn-kpi-card--stacked .cn-kpi-card__body {
384
+ gap: var(--cn-kpi-stacked-gap, 6px);
385
+ }
386
+
387
+ .cn-kpi-card--stacked .cn-kpi-card__title {
388
+ font-size: var(--cn-kpi-stacked-title-size, 14px);
389
+ font-weight: var(--cn-kpi-stacked-title-weight, 400);
390
+ color: var(--color-text-maxcontrast);
391
+ }
392
+
393
+ .cn-kpi-card--stacked .cn-kpi-card__value {
394
+ font-size: var(--cn-kpi-stacked-value-size, 34px);
395
+ font-weight: var(--cn-kpi-value-weight, 700);
396
+ line-height: 1;
397
+ letter-spacing: -0.01em;
398
+ color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
399
+ }
400
+
401
+ .cn-kpi-card--stacked .cn-kpi-card__caption-line {
402
+ flex: 0 0 auto;
403
+ max-width: 100%;
404
+ font-size: var(--cn-kpi-label-size, 13px);
405
+ font-weight: 500;
406
+ }
407
+
408
+ /* A linked stacked tile reads as a tile, not as underlined link text. The
409
+ NL Design theme underlines every `a` with `!important`, so the opt-out
410
+ needs it too; it only applies to this opt-in layout. */
411
+ a.cn-kpi-card--stacked,
412
+ a.cn-kpi-card--stacked:hover,
413
+ a.cn-kpi-card--stacked:focus-visible {
414
+ text-decoration: none !important;
415
+ }
416
+
370
417
  /* Filled: the card draws its own grey box, for a KPI that is NOT already
371
418
  inside a wrapper that draws one. Opt-in via `content.flat: false`. */
372
419
  .cn-kpi-card--filled {
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.46.0",
6
+ "version": "2.47.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -272,6 +272,11 @@
272
272
  "href": { "type": "string" }
273
273
  }
274
274
  },
275
+ "footer": {
276
+ "type": "array",
277
+ "items": { "type": "string", "minLength": 1 },
278
+ "description": "Declare the navigation footer (zuiddrecht-pixel-gaps-2): an ordered list of `section: \"footer\"` menu entry ids, plus the reserved ids `help` (the `nav.help` entry) and `settings` (the settings foldout). Only the named footer entries render there, in that order; footer entries left out move into the settings foldout so their pages stay reachable. `settings` first puts the foldout above the list (the board's `[\"settings\", \"help\"]`). Omitted, the footer renders as before: help, every footer entry, then the foldout."
279
+ },
275
280
  "primaryAction": {
276
281
  "$ref": "#/$defs/primaryAction",
277
282
  "description": "App-wide default primary action rendered as an NcAppNavigationNew button above the main menu list. A page-scoped pages[].primaryAction for the current route wins over this default."
@@ -3603,6 +3608,11 @@
3603
3608
  "default": true,
3604
3609
  "description": "type='dashboard': `false` drops the header row (title, description, header actions, edit toggle) for a page that opens with its own heading, such as a greeting widget; the title stays as a visually hidden heading (zuiddrecht-pixel-gaps). Maps to CnDashboardPage `showHeader`."
3605
3610
  },
3611
+ "showWidgetActions": {
3612
+ "type": "boolean",
3613
+ "default": true,
3614
+ "description": "type='dashboard': `false` drops the overflow Actions menu from every widget that does not set `showActions` itself, for a dashboard whose widget headers carry a header link and nothing else (zuiddrecht-pixel-gaps-2). Maps to CnDashboardPage `showWidgetActions`."
3615
+ },
3606
3616
  "showTitle": {
3607
3617
  "type": "boolean",
3608
3618
  "default": false,
@@ -3626,9 +3636,20 @@
3626
3636
  "label": { "type": "string", "minLength": 1 },
3627
3637
  "route": { "type": "string" },
3628
3638
  "params": { "type": "object", "additionalProperties": true },
3629
- "href": { "type": "string" }
3639
+ "href": { "type": "string" },
3640
+ "icon": { "type": "string", "minLength": 1, "description": "Optional MDI icon name (e.g. `Home`) drawn as the first crumb instead of the label, which then becomes its accessible name (zuiddrecht-pixel-gaps-2). Without it the label shows as text." }
3630
3641
  }
3631
3642
  },
3643
+ "headerCard": {
3644
+ "type": "boolean",
3645
+ "default": false,
3646
+ "description": "type='detail': draw the header (pills, title, actions) as a bordered card on the surface colour, as the board's case header (zuiddrecht-pixel-gaps-2). Maps to CnDetailPage `headerCard`."
3647
+ },
3648
+ "headerWidget": {
3649
+ "type": "string",
3650
+ "minLength": 1,
3651
+ "description": "type='detail': the id of a widget in `widgets` to render inside the header on its own row under the title, without a card of its own, e.g. the stages bars (zuiddrecht-pixel-gaps-2). The widget leaves the body grid. Maps to CnDetailPage `headerWidget`."
3652
+ },
3632
3653
  "mapConfig": {
3633
3654
  "type": "object",
3634
3655
  "additionalProperties": true,