@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.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +99 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +23 -7
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +26 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +6 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +64 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +66 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +22 -5
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +164 -0
- package/dist/esm/store/useObjectStore.js +118 -33
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +14 -0
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +488 -96
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +164 -0
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +148 -7
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -2
- package/src/components/CnDetailPage/CnDetailPage.md +5 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +114 -6
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +34 -6
- package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
- package/src/css/detail-page.css +22 -0
- package/src/css/kpi-card.css +47 -0
- package/src/schemas/app-manifest-v2.schema.json +23 -2
- package/src/store/useObjectStore.js +118 -33
- package/src/utils/actionsDispatcher.js +14 -0
- 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
|
|
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
|
|
2108
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
/**
|
package/src/css/detail-page.css
CHANGED
|
@@ -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;
|
package/src/css/kpi-card.css
CHANGED
|
@@ -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.
|
|
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,
|