@conduction/nextcloud-vue 2.63.0 → 2.64.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 +154 -7
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +87 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +26 -24
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +150 -133
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +37 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +12 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +5 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
- package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +331 -81
- package/dist/esm/utils/schemaRefSlug.js +19 -3
- package/dist/esm/utils/schemaRefSlug.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 +6933 -5958
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +331 -81
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +297 -11
- package/src/components/CnAppRoot/CnAppRoot.md +4 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +94 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +43 -2
- package/src/components/CnDataTable/CnDataTable.vue +45 -1
- package/src/components/CnDetailPage/CnDetailPage.md +4 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
- package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
- package/src/components/CnStagesWidget/stagesModel.js +5 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
- package/src/css/detail-page.css +10 -4
- package/src/css/kpi-card.css +24 -1
- package/src/css/table.css +18 -6
- package/src/schemas/app-manifest-v2.schema.json +113 -3
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain }" :style="wrapperStyle">
|
|
7
|
+
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, '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"
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
{{ ctaLabel }}
|
|
35
35
|
</a>
|
|
36
36
|
</div>
|
|
37
|
+
<!-- A view switch at the right of the heading (`content.views`): "My
|
|
38
|
+
work / My team" as a segmented control whose options are routes. -->
|
|
39
|
+
<div v-if="viewOptions.length > 0" class="cn-header-widget__views">
|
|
40
|
+
<CnSegmentedControl
|
|
41
|
+
:options="viewOptions"
|
|
42
|
+
:modelValue="activeView"
|
|
43
|
+
:ariaLabel="viewsLabel"
|
|
44
|
+
data-testid="cn-header-widget-views"
|
|
45
|
+
@update:modelValue="onViewChange" />
|
|
46
|
+
</div>
|
|
37
47
|
<img
|
|
38
48
|
v-if="hasBackgroundImage"
|
|
39
49
|
class="cn-header-widget__probe"
|
|
@@ -47,6 +57,7 @@
|
|
|
47
57
|
<script>
|
|
48
58
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
49
59
|
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
60
|
+
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
50
61
|
import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
|
|
51
62
|
|
|
52
63
|
const ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']
|
|
@@ -99,6 +110,8 @@ const VERTICAL_ALIGN_FLEX = Object.freeze({
|
|
|
99
110
|
export default {
|
|
100
111
|
name: 'CnHeaderWidget',
|
|
101
112
|
|
|
113
|
+
components: { CnSegmentedControl },
|
|
114
|
+
|
|
102
115
|
inject: {
|
|
103
116
|
/**
|
|
104
117
|
* Host translate function provided by CnAppRoot as
|
|
@@ -115,9 +128,12 @@ export default {
|
|
|
115
128
|
* Persisted widget content: `{title, subtitle, backgroundImageUrl,
|
|
116
129
|
* backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
|
|
117
130
|
* overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
|
|
118
|
-
* greeting, showDate, plain}`. All fields are optional except
|
|
119
|
-
* (or `greeting`); unknown enum values collapse to documented
|
|
120
|
-
* and the renderer never throws.
|
|
131
|
+
* greeting, showDate, plain, views}`. All fields are optional except
|
|
132
|
+
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
133
|
+
* defaults and the renderer never throws. `views` is
|
|
134
|
+
* `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented
|
|
135
|
+
* control at the right of the heading whose checked option is the
|
|
136
|
+
* current route; choosing another pushes its route.
|
|
121
137
|
*
|
|
122
138
|
* @type {object}
|
|
123
139
|
*/
|
|
@@ -198,6 +214,55 @@ export default {
|
|
|
198
214
|
return this.now instanceof Date ? this.now : new Date()
|
|
199
215
|
},
|
|
200
216
|
|
|
217
|
+
/**
|
|
218
|
+
* The view switch's options from `content.views.options`: each
|
|
219
|
+
* `{ label, route, params? }` with a non-empty label and route becomes
|
|
220
|
+
* a segmented-control option keyed on its route name (the label goes
|
|
221
|
+
* through the host translate function). Empty without a router, so
|
|
222
|
+
* the switch cannot offer routes it cannot follow.
|
|
223
|
+
*
|
|
224
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
225
|
+
* @return {Array<{value: string, label: string}>}
|
|
226
|
+
*/
|
|
227
|
+
viewOptions() {
|
|
228
|
+
const raw = this.content && this.content.views && this.content.views.options
|
|
229
|
+
if (!Array.isArray(raw) || !this.$router) {
|
|
230
|
+
return []
|
|
231
|
+
}
|
|
232
|
+
return raw
|
|
233
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')
|
|
234
|
+
.map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The checked view: the option whose route is the current route, else
|
|
239
|
+
* the first option (a switch always has one checked option).
|
|
240
|
+
*
|
|
241
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
242
|
+
* @return {string|null}
|
|
243
|
+
*/
|
|
244
|
+
activeView() {
|
|
245
|
+
const options = this.viewOptions
|
|
246
|
+
if (options.length === 0) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
const current = this.$route && this.$route.name
|
|
250
|
+
const match = options.find((o) => o.value === current)
|
|
251
|
+
return match ? match.value : options[0].value
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Accessible name of the view switch: `content.views.ariaLabel`
|
|
256
|
+
* through the host translate function, else "View".
|
|
257
|
+
*
|
|
258
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
259
|
+
* @return {string}
|
|
260
|
+
*/
|
|
261
|
+
viewsLabel() {
|
|
262
|
+
const label = this.content && this.content.views && this.content.views.ariaLabel
|
|
263
|
+
return label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')
|
|
264
|
+
},
|
|
265
|
+
|
|
201
266
|
/**
|
|
202
267
|
* Whether the plain presentation is on (no coloured background).
|
|
203
268
|
*
|
|
@@ -490,7 +555,9 @@ export default {
|
|
|
490
555
|
return {
|
|
491
556
|
position: 'relative',
|
|
492
557
|
'z-index': 1,
|
|
493
|
-
|
|
558
|
+
// Beside a view switch the content shares the row instead of
|
|
559
|
+
// taking the whole width (see .cn-header-widget--with-views).
|
|
560
|
+
width: this.viewOptions.length > 0 ? 'auto' : '100%',
|
|
494
561
|
height: '100%',
|
|
495
562
|
display: 'flex',
|
|
496
563
|
'flex-direction': 'column',
|
|
@@ -604,6 +671,26 @@ export default {
|
|
|
604
671
|
},
|
|
605
672
|
|
|
606
673
|
methods: {
|
|
674
|
+
/**
|
|
675
|
+
* Follow the chosen view: push its route (with the option's `params`
|
|
676
|
+
* when declared). Nothing happens for the route already shown.
|
|
677
|
+
*
|
|
678
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
679
|
+
* @param {string} route The chosen option's route name.
|
|
680
|
+
* @return {void}
|
|
681
|
+
*/
|
|
682
|
+
onViewChange(route) {
|
|
683
|
+
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
const declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}
|
|
687
|
+
const target = { name: route }
|
|
688
|
+
if (declared.params && typeof declared.params === 'object') {
|
|
689
|
+
target.params = declared.params
|
|
690
|
+
}
|
|
691
|
+
this.$router.push(target)
|
|
692
|
+
},
|
|
693
|
+
|
|
607
694
|
/**
|
|
608
695
|
* Mark the background image as broken so the renderer falls back to
|
|
609
696
|
* the solid colour.
|
|
@@ -655,6 +742,29 @@ export default {
|
|
|
655
742
|
border-radius: 0;
|
|
656
743
|
}
|
|
657
744
|
|
|
745
|
+
/* With a view switch the heading and the switch share one row, the switch
|
|
746
|
+
at the right and bottom-aligned with the heading; on a narrow card the
|
|
747
|
+
switch wraps under it. */
|
|
748
|
+
.cn-header-widget--with-views {
|
|
749
|
+
display: flex;
|
|
750
|
+
flex-wrap: wrap;
|
|
751
|
+
align-items: flex-end;
|
|
752
|
+
justify-content: space-between;
|
|
753
|
+
gap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.cn-header-widget--with-views .cn-header-widget__content {
|
|
757
|
+
flex: 1 1 320px;
|
|
758
|
+
width: auto;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.cn-header-widget__views {
|
|
762
|
+
position: relative;
|
|
763
|
+
z-index: 1;
|
|
764
|
+
flex: 0 0 auto;
|
|
765
|
+
padding: 16px;
|
|
766
|
+
}
|
|
767
|
+
|
|
658
768
|
.cn-header-widget__date {
|
|
659
769
|
font-size: 14px;
|
|
660
770
|
line-height: 1.4;
|
|
@@ -726,7 +836,7 @@ export default {
|
|
|
726
836
|
height: 1px;
|
|
727
837
|
opacity: 0;
|
|
728
838
|
pointer-events: none;
|
|
729
|
-
|
|
839
|
+
inset-inline-start: -9999px;
|
|
730
840
|
top: -9999px;
|
|
731
841
|
}
|
|
732
842
|
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
<slot
|
|
14
14
|
name="header"
|
|
15
15
|
:title="title"
|
|
16
|
-
:description="
|
|
16
|
+
:description="headerDescription"
|
|
17
17
|
:icon="resolvedIcon"
|
|
18
18
|
:showTitle="showTitle">
|
|
19
19
|
<CnPageHeader
|
|
20
20
|
:title="title"
|
|
21
|
-
:description="
|
|
21
|
+
:description="headerDescription"
|
|
22
22
|
:icon="resolvedIcon"
|
|
23
23
|
:visuallyHidden="!showTitle" />
|
|
24
24
|
</slot>
|
|
@@ -1177,6 +1177,21 @@ export default {
|
|
|
1177
1177
|
default: '',
|
|
1178
1178
|
},
|
|
1179
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* A description that carries the collection's total: `{total}` in
|
|
1182
|
+
* the text is replaced by the current pagination total ("{total}
|
|
1183
|
+
* open cases" reads "48 open cases"). Rendered in place of
|
|
1184
|
+
* `description` once a total is known; before that, `description`
|
|
1185
|
+
* shows. Manifest key `config.countSubtitle`. Goes through the host
|
|
1186
|
+
* translate function.
|
|
1187
|
+
*
|
|
1188
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
|
|
1189
|
+
*/
|
|
1190
|
+
countSubtitle: {
|
|
1191
|
+
type: String,
|
|
1192
|
+
default: '',
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1180
1195
|
/**
|
|
1181
1196
|
* Whether to show the page header (icon, title, description) inline.
|
|
1182
1197
|
* When false (default), only the sidebar header shows the title
|
|
@@ -4231,6 +4246,21 @@ export default {
|
|
|
4231
4246
|
return resolveDeepTokens(seed, {})
|
|
4232
4247
|
},
|
|
4233
4248
|
|
|
4249
|
+
/**
|
|
4250
|
+
* The header's description line: `countSubtitle` with the total
|
|
4251
|
+
* filled in when one is known, else `description`.
|
|
4252
|
+
*
|
|
4253
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
|
|
4254
|
+
* @return {string}
|
|
4255
|
+
*/
|
|
4256
|
+
headerDescription() {
|
|
4257
|
+
const total = this.effectivePagination?.total
|
|
4258
|
+
if (this.countSubtitle && typeof total === 'number' && total >= 0) {
|
|
4259
|
+
return this.cnTranslate(this.countSubtitle).replace('{total}', String(total))
|
|
4260
|
+
}
|
|
4261
|
+
return this.description
|
|
4262
|
+
},
|
|
4263
|
+
|
|
4234
4264
|
/** Add button label — derived from schema.title if not explicitly set */
|
|
4235
4265
|
resolvedAddLabel() {
|
|
4236
4266
|
if (this.addLabel) {
|
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
:sortKey="localSort.field || null"
|
|
152
152
|
:sortOrder="localSort.dir || 'asc'"
|
|
153
153
|
borderless
|
|
154
|
+
:hideHeader="content.hideHeader === true"
|
|
154
155
|
@rowClick="onRowClick"
|
|
155
156
|
@rowAuxClick="onRowAuxClick"
|
|
156
157
|
@select="onSelect"
|
|
@@ -172,6 +173,7 @@
|
|
|
172
173
|
:sortKey="localSort.field || null"
|
|
173
174
|
:sortOrder="localSort.dir || 'asc'"
|
|
174
175
|
borderless
|
|
176
|
+
:hideHeader="content.hideHeader === true"
|
|
175
177
|
@rowClick="onRowClick"
|
|
176
178
|
@rowAuxClick="onRowAuxClick"
|
|
177
179
|
@select="onSelect"
|
|
@@ -419,7 +421,11 @@ export default {
|
|
|
419
421
|
* those carrying a selected value. Client-side over the fetched page,
|
|
420
422
|
* like the rest of this widget's row set.
|
|
421
423
|
*
|
|
422
|
-
*
|
|
424
|
+
* `hideHeader: true` drops the table's column header row (CnDataTable's
|
|
425
|
+
* `hideHeader`), for a short list that reads as rows, not as a table.
|
|
426
|
+
*
|
|
427
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
|
|
428
|
+
* @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
|
|
423
429
|
*/
|
|
424
430
|
content: {
|
|
425
431
|
type: Object,
|
|
@@ -35,7 +35,45 @@
|
|
|
35
35
|
{{ tr('No stages to show') }}
|
|
36
36
|
</p>
|
|
37
37
|
<template v-else>
|
|
38
|
+
<!-- The bars variant (`variant: "bars"`): equal columns, a thin bar
|
|
39
|
+
per step coloured by state, the label under it and an optional
|
|
40
|
+
date line. No description: eight Woo steps with their
|
|
41
|
+
descriptions crowded into five lines and clipped the first
|
|
42
|
+
label. The current step says so for a screen reader. -->
|
|
43
|
+
<ol
|
|
44
|
+
v-if="isBars"
|
|
45
|
+
class="cn-stages-widget__bars"
|
|
46
|
+
:aria-label="ariaLabel"
|
|
47
|
+
data-testid="cn-stages-widget-bars">
|
|
48
|
+
<li
|
|
49
|
+
v-for="stage in barStages"
|
|
50
|
+
:key="stage.id"
|
|
51
|
+
class="cn-stages-widget__bar-step"
|
|
52
|
+
:class="`cn-stages-widget__bar-step--${stage.state}`"
|
|
53
|
+
:aria-current="stage.state === 'current' ? 'step' : null"
|
|
54
|
+
:data-testid="`cn-stages-widget-bar-${stage.id}`">
|
|
55
|
+
<span class="cn-stages-widget__bar" aria-hidden="true" />
|
|
56
|
+
<component
|
|
57
|
+
:is="stage.clickable ? 'button' : 'span'"
|
|
58
|
+
:type="stage.clickable ? 'button' : null"
|
|
59
|
+
class="cn-stages-widget__bar-label"
|
|
60
|
+
:title="stage.hint || stage.label"
|
|
61
|
+
:aria-disabled="stage.disabled && stage.clickable ? 'true' : null"
|
|
62
|
+
:data-testid="`cn-stages-widget-stage-${stage.id}`"
|
|
63
|
+
@click="onBarClick(stage)">
|
|
64
|
+
{{ stage.label }}<span v-if="stage.state === 'current'" class="cn-stages-widget__sr-only"> ({{ tr('current step') }})</span>
|
|
65
|
+
</component>
|
|
66
|
+
<span v-if="stage.date" class="cn-stages-widget__bar-date">{{ stage.date }}</span>
|
|
67
|
+
<span
|
|
68
|
+
v-if="stage.reason"
|
|
69
|
+
:class="stage.reasonVisible ? 'cn-stages-widget__reason' : 'cn-stages-widget__sr-only'"
|
|
70
|
+
:data-testid="`cn-stages-widget-reason-${stage.id}`">
|
|
71
|
+
{{ stage.reason }}
|
|
72
|
+
</span>
|
|
73
|
+
</li>
|
|
74
|
+
</ol>
|
|
38
75
|
<CnTimelineStages
|
|
76
|
+
v-else
|
|
39
77
|
:stages="timelineStages"
|
|
40
78
|
:currentStage="currentStageId"
|
|
41
79
|
:orientation="orientation"
|
|
@@ -310,13 +348,15 @@ export default {
|
|
|
310
348
|
* `currentField`, `stagesEndpoint` or `stagesSource`, `transition`
|
|
311
349
|
* (`{ kind: 'lifecycle' }` by default, or `{ kind: 'field' }`, or
|
|
312
350
|
* absent for a read-only strip), `unreachableReason`, `orientation`,
|
|
313
|
-
* `size`, `ariaLabel` and `
|
|
351
|
+
* `size`, `ariaLabel`, `label` and `variant` (`"bars"` draws a thin
|
|
352
|
+
* coloured bar per step with the label under it and no description;
|
|
353
|
+
* the source's `dateField` adds a date line).
|
|
314
354
|
*
|
|
315
355
|
* `ariaLabel` names the strip for a screen reader. It falls back to
|
|
316
356
|
* `label`, the placement's own title, so a titled card does not have to
|
|
317
357
|
* repeat itself, and then to "Stages".
|
|
318
358
|
*
|
|
319
|
-
* @type {{currentField?: string, stagesEndpoint?: object, stagesSource?: object, transition?: object, unreachableReason?: string, orientation?: ('horizontal'|'vertical'), size?: ('medium'|'small'), ariaLabel?: string, label?: string}}
|
|
359
|
+
* @type {{currentField?: string, stagesEndpoint?: object, stagesSource?: object, transition?: object, unreachableReason?: string, orientation?: ('horizontal'|'vertical'), size?: ('medium'|'small'), ariaLabel?: string, label?: string, variant?: ('dots'|'bars')}}
|
|
320
360
|
*/
|
|
321
361
|
content: {
|
|
322
362
|
type: Object,
|
|
@@ -723,6 +763,45 @@ export default {
|
|
|
723
763
|
return label ? this.effectiveTranslate(label) : this.tr('Stages')
|
|
724
764
|
},
|
|
725
765
|
|
|
766
|
+
/**
|
|
767
|
+
* Whether the bars variant is on (`content.variant: "bars"`). Off by
|
|
768
|
+
* default: the dot strip renders as it always has.
|
|
769
|
+
*
|
|
770
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-stages-widget-draws-bars
|
|
771
|
+
* @return {boolean}
|
|
772
|
+
*/
|
|
773
|
+
isBars() {
|
|
774
|
+
return this.content.variant === 'bars'
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* The stages for the bars variant: each with its `state` (`done`
|
|
779
|
+
* before the current stage, `current`, `todo` after it, every stage
|
|
780
|
+
* `todo` when none is current), its `date`, and whether it is a
|
|
781
|
+
* button (the strip is interactive and the stage is not the current
|
|
782
|
+
* one).
|
|
783
|
+
*
|
|
784
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-stages-widget-draws-bars
|
|
785
|
+
* @return {Array<object>}
|
|
786
|
+
*/
|
|
787
|
+
barStages() {
|
|
788
|
+
const current = this.stages.findIndex((s) => s.id === this.currentStageId)
|
|
789
|
+
return this.timelineStages.map((stage, index) => {
|
|
790
|
+
let state = 'todo'
|
|
791
|
+
if (current >= 0 && index < current) {
|
|
792
|
+
state = 'done'
|
|
793
|
+
} else if (index === current) {
|
|
794
|
+
state = 'current'
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
...stage,
|
|
798
|
+
date: this.stages[index]?.date || '',
|
|
799
|
+
state,
|
|
800
|
+
clickable: this.interactive && state !== 'current',
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
},
|
|
804
|
+
|
|
726
805
|
/** @return {'horizontal'|'vertical'} The layout direction. */
|
|
727
806
|
orientation() {
|
|
728
807
|
return this.content.orientation === 'vertical' ? 'vertical' : 'horizontal'
|
|
@@ -969,6 +1048,26 @@ export default {
|
|
|
969
1048
|
* @param {{stage: {id: string}}} payload The CnTimelineStages event.
|
|
970
1049
|
* @return {void}
|
|
971
1050
|
*/
|
|
1051
|
+
/**
|
|
1052
|
+
* A click on a bar-variant step: a disabled step reports why (as
|
|
1053
|
+
* CnTimelineStages does through `stage-blocked`), any other goes
|
|
1054
|
+
* through the shared move path.
|
|
1055
|
+
*
|
|
1056
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-stages-widget-draws-bars
|
|
1057
|
+
* @param {object} stage The bar step.
|
|
1058
|
+
* @return {void}
|
|
1059
|
+
*/
|
|
1060
|
+
onBarClick(stage) {
|
|
1061
|
+
if (!stage.clickable) {
|
|
1062
|
+
return
|
|
1063
|
+
}
|
|
1064
|
+
if (stage.disabled) {
|
|
1065
|
+
this.onStageBlocked({ stage })
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
this.onStageClick({ stage })
|
|
1069
|
+
},
|
|
1070
|
+
|
|
972
1071
|
onStageClick({ stage }) {
|
|
973
1072
|
if (!this.canMove || !stage) {
|
|
974
1073
|
return
|
|
@@ -1340,6 +1439,85 @@ export default {
|
|
|
1340
1439
|
color: var(--color-text-maxcontrast);
|
|
1341
1440
|
}
|
|
1342
1441
|
|
|
1442
|
+
/* The bars variant: equal columns, a thin bar per step. Theme hooks:
|
|
1443
|
+
--cn-stages-bar-height, --cn-stages-bar-radius, --cn-stages-bar-done,
|
|
1444
|
+
--cn-stages-bar-current, --cn-stages-bar-todo. */
|
|
1445
|
+
.cn-stages-widget__bars {
|
|
1446
|
+
display: grid;
|
|
1447
|
+
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
|
1448
|
+
grid-auto-flow: column;
|
|
1449
|
+
gap: 8px;
|
|
1450
|
+
margin: 0;
|
|
1451
|
+
padding: 0;
|
|
1452
|
+
list-style: none;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.cn-stages-widget__bar-step {
|
|
1456
|
+
display: flex;
|
|
1457
|
+
flex-direction: column;
|
|
1458
|
+
gap: 8px;
|
|
1459
|
+
min-width: 0;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.cn-stages-widget__bar {
|
|
1463
|
+
display: block;
|
|
1464
|
+
height: var(--cn-stages-bar-height, 6px);
|
|
1465
|
+
border-radius: var(--cn-stages-bar-radius, 3px);
|
|
1466
|
+
background: var(--cn-stages-bar-todo, var(--color-border));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
.cn-stages-widget__bar-step--done .cn-stages-widget__bar {
|
|
1470
|
+
background: var(--cn-stages-bar-done, var(--color-primary-element));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.cn-stages-widget__bar-step--current .cn-stages-widget__bar {
|
|
1474
|
+
background: var(--cn-stages-bar-current, var(--color-error));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.cn-stages-widget__bar-label {
|
|
1478
|
+
display: block;
|
|
1479
|
+
margin: 0;
|
|
1480
|
+
padding: 0;
|
|
1481
|
+
border: 0;
|
|
1482
|
+
background: none;
|
|
1483
|
+
color: var(--color-text-maxcontrast);
|
|
1484
|
+
font: inherit;
|
|
1485
|
+
font-size: 14px;
|
|
1486
|
+
text-align: start;
|
|
1487
|
+
overflow: hidden;
|
|
1488
|
+
text-overflow: ellipsis;
|
|
1489
|
+
white-space: nowrap;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
button.cn-stages-widget__bar-label {
|
|
1493
|
+
cursor: pointer;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
button.cn-stages-widget__bar-label[aria-disabled="true"] {
|
|
1497
|
+
cursor: not-allowed;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
button.cn-stages-widget__bar-label:focus-visible {
|
|
1501
|
+
outline: 2px solid var(--color-primary-element);
|
|
1502
|
+
outline-offset: 2px;
|
|
1503
|
+
border-radius: var(--border-radius-small, 4px);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
.cn-stages-widget__bar-step--done .cn-stages-widget__bar-label {
|
|
1507
|
+
color: var(--color-main-text);
|
|
1508
|
+
font-weight: 600;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.cn-stages-widget__bar-step--current .cn-stages-widget__bar-label {
|
|
1512
|
+
color: var(--color-main-text);
|
|
1513
|
+
font-weight: 700;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.cn-stages-widget__bar-date {
|
|
1517
|
+
font-size: 12px;
|
|
1518
|
+
color: var(--color-text-maxcontrast);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1343
1521
|
/* Text for screen readers only. */
|
|
1344
1522
|
.cn-stages-widget__sr-only {
|
|
1345
1523
|
position: absolute;
|
|
@@ -85,8 +85,9 @@ export function isTrue(value) {
|
|
|
85
85
|
* the rows keep the order they arrived in.
|
|
86
86
|
*
|
|
87
87
|
* @param {Array<object>} rows The stage rows.
|
|
88
|
-
* @
|
|
89
|
-
* @
|
|
88
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-stages-widget-draws-bars
|
|
89
|
+
* @param {{idField?: string, labelField?: string, descriptionField?: string, dateField?: string, orderField?: string, finalField?: string}} [cfg] The field mapping.
|
|
90
|
+
* @return {Array<{id: string, label: string, subtitle: string, date: string, final: boolean}>} The stages.
|
|
90
91
|
*/
|
|
91
92
|
export function normalizeStages(rows, cfg = {}) {
|
|
92
93
|
const list = Array.isArray(rows) ? rows.filter((r) => r && typeof r === 'object') : []
|
|
@@ -108,6 +109,8 @@ export function normalizeStages(rows, cfg = {}) {
|
|
|
108
109
|
id: rowId(row, cfg.idField),
|
|
109
110
|
label,
|
|
110
111
|
subtitle: cfg.descriptionField ? labelText(getByPath(row, cfg.descriptionField)) : '',
|
|
112
|
+
// An optional date line (`dateField`), drawn by the bars variant.
|
|
113
|
+
date: cfg.dateField ? labelText(getByPath(row, cfg.dateField)) : '',
|
|
111
114
|
final: cfg.finalField ? isTrue(getByPath(row, cfg.finalField)) : false,
|
|
112
115
|
}
|
|
113
116
|
})
|
|
@@ -94,7 +94,11 @@
|
|
|
94
94
|
{{ formattedTrend }}
|
|
95
95
|
</span>
|
|
96
96
|
</template>
|
|
97
|
-
<span
|
|
97
|
+
<span
|
|
98
|
+
v-if="!displayLoading && !displayError && resolvedCaption"
|
|
99
|
+
class="cn-kpi-card__label cn-stat-widget__caption"
|
|
100
|
+
:class="captionVariantClass"
|
|
101
|
+
data-testid="cn-stat-widget-caption">
|
|
98
102
|
{{ resolvedCaption }}
|
|
99
103
|
</span>
|
|
100
104
|
</div>
|
|
@@ -461,7 +465,7 @@ export default {
|
|
|
461
465
|
* counted between LOCAL CALENDAR DAYS, not as elapsed milliseconds, so
|
|
462
466
|
* the time of day on a deadline never moves the answer.
|
|
463
467
|
*
|
|
464
|
-
* @type {{label?: string, icon?: string, iconColor?: string, valueColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {kind?: string, register?: string, schema?: string, metric?: string, field?: string, filter?: object, url?: string, path?: string, params?: object}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, limitField?: string, limit?: number, dateRange?: {presets?: Array<{id: string, label?: string, from?: string, to?: string}>, default?: string}, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down'), variant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), variantWhen?: Array<{op: string, value: unknown, variant: string, icon?: string}>, objectField?: (string|{field: string, resolve?: {register: string, schema: string, labelField?: string, variantField?: string, variantMap?: {[key: string]: string}}}), display?: ('text'|'badge'|'countdown'), countdown?: {unit?: 'days', warnAt?: number, dangerAt?: number, futureLabel?: string, todayLabel?: string, pastLabel?: string, emptyText?: string}, emptyText?: string, overrides?: Array<{when: {field: string, op?: string, value?: unknown}, label?: string, variant?: string, icon?: string}>}}
|
|
468
|
+
* @type {{label?: string, icon?: string, iconColor?: string, valueColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {kind?: string, register?: string, schema?: string, metric?: string, field?: string, filter?: object, url?: string, path?: string, params?: object}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, limitField?: string, limit?: number, dateRange?: {presets?: Array<{id: string, label?: string, from?: string, to?: string}>, default?: string}, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down'), variant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), variantWhen?: Array<{op: string, value: unknown, variant: string, icon?: string}>, objectField?: (string|{field: string, resolve?: {register: string, schema: string, labelField?: string, variantField?: string, variantMap?: {[key: string]: string}}}), display?: ('text'|'badge'|'countdown'), countdown?: {unit?: 'days', warnAt?: number, dangerAt?: number, futureLabel?: string, todayLabel?: string, pastLabel?: string, emptyText?: string}, emptyText?: string, captionVariant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), captionVariantWhen?: Array<{op: string, value: unknown, variant: string}>, overrides?: Array<{when: {field: string, op?: string, value?: unknown}, label?: string, variant?: string, icon?: string, caption?: string, captionVariant?: string}>}}
|
|
465
469
|
*/
|
|
466
470
|
content: {
|
|
467
471
|
type: Object,
|
|
@@ -605,7 +609,10 @@ export default {
|
|
|
605
609
|
* @return {string}
|
|
606
610
|
*/
|
|
607
611
|
resolvedCaption() {
|
|
608
|
-
const
|
|
612
|
+
const override = this.activeOverride
|
|
613
|
+
const caption = (override && typeof override.caption === 'string' && override.caption !== '')
|
|
614
|
+
? override.caption
|
|
615
|
+
: this.content.caption
|
|
609
616
|
if (!caption) {
|
|
610
617
|
return ''
|
|
611
618
|
}
|
|
@@ -615,11 +622,58 @@ export default {
|
|
|
615
622
|
}
|
|
616
623
|
const payload = this.endpointMode ? this.epData : null
|
|
617
624
|
return translated.replace(/\{([A-Za-z0-9_.]+)\}/g, (whole, path) => {
|
|
625
|
+
// `{value}` is the tile's own number, so a caption can read
|
|
626
|
+
// "{value} due today" on a register-counted tile that has no
|
|
627
|
+
// payload to read fields from.
|
|
628
|
+
if (path === 'value' && (payload === null || getByPath(payload, path) === undefined)) {
|
|
629
|
+
return this.formattedValue === undefined || this.formattedValue === null ? '' : String(this.formattedValue)
|
|
630
|
+
}
|
|
618
631
|
const v = getByPath(payload, path)
|
|
619
632
|
return (v === undefined || v === null) ? '' : String(v)
|
|
620
633
|
}).replace(/\s{2,}/g, ' ').trim()
|
|
621
634
|
},
|
|
622
635
|
|
|
636
|
+
/**
|
|
637
|
+
* The first `content.captionVariantWhen` rule the tile's value
|
|
638
|
+
* matches (`[{ op, value, variant }]`, the `variantWhen` shape), or
|
|
639
|
+
* null. Colours the caption on the value without recolouring the
|
|
640
|
+
* tile: "1 due today" turns red while the number stays ink.
|
|
641
|
+
*
|
|
642
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-stat-tile-colours-its-caption-by-rule
|
|
643
|
+
* @return {object|null}
|
|
644
|
+
*/
|
|
645
|
+
activeCaptionRule() {
|
|
646
|
+
const rules = this.content.captionVariantWhen
|
|
647
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
648
|
+
return null
|
|
649
|
+
}
|
|
650
|
+
const current = this.displayValue
|
|
651
|
+
if (current === null || current === undefined) {
|
|
652
|
+
return null
|
|
653
|
+
}
|
|
654
|
+
return rules.find((r) => r && this.matchesRule(current, r)) || null
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* The caption's colour class: the matching override's
|
|
659
|
+
* `captionVariant`, else `content.captionVariant`, as
|
|
660
|
+
* `cn-kpi-card__label--<variant>`. '' for `default` or an unknown
|
|
661
|
+
* variant, so an uncoloured caption is what it always was.
|
|
662
|
+
*
|
|
663
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-stat-tile-colours-its-caption-by-rule
|
|
664
|
+
* @return {string}
|
|
665
|
+
*/
|
|
666
|
+
captionVariantClass() {
|
|
667
|
+
const override = this.activeOverride
|
|
668
|
+
const variant = (override && override.captionVariant)
|
|
669
|
+
|| (this.activeCaptionRule && this.activeCaptionRule.variant)
|
|
670
|
+
|| this.content.captionVariant
|
|
671
|
+
if (!variant || variant === 'default' || !Object.hasOwn(VARIANT_COLORS, variant)) {
|
|
672
|
+
return ''
|
|
673
|
+
}
|
|
674
|
+
return `cn-kpi-card__label--${variant === 'danger' ? 'error' : variant}`
|
|
675
|
+
},
|
|
676
|
+
|
|
623
677
|
/**
|
|
624
678
|
* The unwrapped detail-page object context for token resolution, or null
|
|
625
679
|
* on surfaces (dashboards) that don't provide one.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<CnTabs
|
|
9
9
|
:aria-label="stripLabel"
|
|
10
10
|
:moreLabel="moreLabel"
|
|
11
|
+
:variant="stripVariant"
|
|
11
12
|
class="cn-tabs-widget__tabs"
|
|
12
13
|
@update:activeIndex="onTabChange">
|
|
13
14
|
<!-- One Actions menu for the whole widget, bound to whichever child
|
|
@@ -246,9 +247,10 @@ export default {
|
|
|
246
247
|
*
|
|
247
248
|
* `maxVisibleTabs` caps the strip: later tabs go under "More".
|
|
248
249
|
* `hideEmpty: true` moves a tab whose count is 0 there too.
|
|
249
|
-
* `moreLabel` names that menu.
|
|
250
|
+
* `moreLabel` names that menu. `variant: "segmented"` draws the strip
|
|
251
|
+
* as a pill switch (CnTabs' segmented variant); `line` is the default.
|
|
250
252
|
*
|
|
251
|
-
* @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string }}
|
|
253
|
+
* @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
|
|
252
254
|
*/
|
|
253
255
|
content: {
|
|
254
256
|
type: Object,
|
|
@@ -464,6 +466,17 @@ export default {
|
|
|
464
466
|
return this.activeTab?.label || ''
|
|
465
467
|
},
|
|
466
468
|
|
|
469
|
+
/**
|
|
470
|
+
* The strip's look: `segmented` when the content asks for it, else
|
|
471
|
+
* CnTabs' default `line`. An unknown value falls back to `line`.
|
|
472
|
+
*
|
|
473
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-tabs-widget-renders-a-segmented-strip
|
|
474
|
+
* @return {('line'|'segmented')} The CnTabs variant.
|
|
475
|
+
*/
|
|
476
|
+
stripVariant() {
|
|
477
|
+
return this.content?.variant === 'segmented' ? 'segmented' : 'line'
|
|
478
|
+
},
|
|
479
|
+
|
|
467
480
|
/**
|
|
468
481
|
* Accessible name for the tab strip.
|
|
469
482
|
*
|
|
@@ -163,3 +163,7 @@ Request a feature / Report a bug / Documentation render on every widget; the sha
|
|
|
163
163
|
## High contrast
|
|
164
164
|
|
|
165
165
|
`high-contrast` declares that this widget wants a high contrast treatment. The flag reaches the theme; the component picks no colour. See [the reference page](../../../docs/components/cn-widget-wrapper.md#high-contrast).
|
|
166
|
+
|
|
167
|
+
### A text link in the header (`headerLink`)
|
|
168
|
+
|
|
169
|
+
`headerLink` (`{ label, route?, params?, query?, href? }`) draws one text link in the header before the overflow menu, such as "All deadlines".
|