@conduction/nextcloud-vue 2.64.0 → 2.66.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 +125 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +439 -381
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +142 -26
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +195 -124
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +84 -11
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +3 -2
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.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/l10n/en.json.js +3 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +400 -0
- package/dist/esm/mixins/pageViews.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +182 -0
- package/dist/esm/store/useObjectStore.js +118 -33
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +25 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +27 -3
- package/dist/esm/utils/columnFilters.js.map +1 -1
- package/dist/esm/utils/schema.js +40 -2
- package/dist/esm/utils/schema.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 +2001 -684
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +182 -0
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -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 +194 -10
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDetailPage/CnDetailPage.md +5 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +208 -11
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +100 -13
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
- package/src/css/detail-page.css +27 -0
- package/src/css/kpi-card.css +47 -0
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +51 -2
- package/src/store/useObjectStore.js +118 -33
- package/src/utils/actionsDispatcher.js +25 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
import { readUserPreference, writeUserPreference, USER_PREFERENCE_STORAGE_PREFIX } from '../composables/useUserPreferences.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* Page views: a few views of one page, each its own widget grid, behind one
|
|
9
|
+
* segmented control ("My work | My team").
|
|
10
|
+
*
|
|
11
|
+
* A page that mixes this in accepts `views` (each `{ id, label, icon?,
|
|
12
|
+
* widgets, layout, emptyText? }`), `defaultView` and `viewsLabel`. It keeps
|
|
13
|
+
* the chosen view, writes it to the address (`?view=<id>`) and to the
|
|
14
|
+
* user's preferences (the server, mirrored in the browser), and provides `cnPageViews` so a greeting header widget on
|
|
15
|
+
* the page can draw the switch instead of the page.
|
|
16
|
+
*
|
|
17
|
+
* Which view opens: the address, then the stored view, then `defaultView`,
|
|
18
|
+
* then the first view. An id that names no view is ignored at every step.
|
|
19
|
+
*
|
|
20
|
+
* Used by CnDashboardPage and CnDetailPage. Each renders the chosen view's
|
|
21
|
+
* `layout` with its own grid, so a view looks exactly like the page's own
|
|
22
|
+
* grid.
|
|
23
|
+
*
|
|
24
|
+
* @mixin pageViews
|
|
25
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The query parameter that carries the chosen view. */
|
|
29
|
+
const PAGE_VIEW_QUERY_KEY = 'view';
|
|
30
|
+
|
|
31
|
+
/** Prefix of the user-preference key; the page id follows it. */
|
|
32
|
+
const PAGE_VIEW_PREFERENCE_PREFIX = 'cn_page_view:';
|
|
33
|
+
|
|
34
|
+
let regionSeq = 0;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read the browser mirror of a user preference synchronously, so the stored
|
|
38
|
+
* view is on screen from the first frame. Errors read as nothing stored.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} appId The app id.
|
|
41
|
+
* @param {string} key The preference key.
|
|
42
|
+
* @return {string|null} The stored id, or null.
|
|
43
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
|
|
44
|
+
*/
|
|
45
|
+
function readStoredViewMirror(appId, key) {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof localStorage === 'undefined') {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
const raw = localStorage.getItem(`${USER_PREFERENCE_STORAGE_PREFIX}${appId}:${key}`);
|
|
51
|
+
if (raw === null || raw === undefined) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
const value = JSON.parse(raw);
|
|
55
|
+
return typeof value === 'string' && value !== '' ? value : null
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pageViews = {
|
|
62
|
+
inject: {
|
|
63
|
+
/** The host app id, provided by CnAppRoot; keys the stored view. */
|
|
64
|
+
cnPageViewsAppId: { from: 'cnAppId', default: '' },
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
props: {
|
|
68
|
+
/**
|
|
69
|
+
* Views of this page, each its own widget grid behind one segmented
|
|
70
|
+
* control. Each is `{ id, label, icon?, widgets, layout, emptyText? }`:
|
|
71
|
+
* `widgets` and `layout` have the same shape as the page's own. The
|
|
72
|
+
* chosen view renders in a region below the page's own grid. Empty
|
|
73
|
+
* (the default) draws no switch and no region.
|
|
74
|
+
*
|
|
75
|
+
* @type {Array<{id: string, label: string, icon?: string, widgets?: Array<object>, layout?: Array<object>, emptyText?: string}>}
|
|
76
|
+
*/
|
|
77
|
+
views: {
|
|
78
|
+
type: Array,
|
|
79
|
+
default: () => [],
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The id of the view chosen when neither the address nor the browser
|
|
84
|
+
* store names one. Empty chooses the first view.
|
|
85
|
+
*
|
|
86
|
+
* @type {string}
|
|
87
|
+
*/
|
|
88
|
+
defaultView: {
|
|
89
|
+
type: String,
|
|
90
|
+
default: '',
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Accessible name of the view switch (through the host translate
|
|
95
|
+
* function). Empty reads "View".
|
|
96
|
+
*
|
|
97
|
+
* @type {string}
|
|
98
|
+
*/
|
|
99
|
+
viewsLabel: {
|
|
100
|
+
type: String,
|
|
101
|
+
default: '',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
data() {
|
|
106
|
+
regionSeq += 1;
|
|
107
|
+
return {
|
|
108
|
+
/** The view the user chose in this session, or the stored one. */
|
|
109
|
+
selectedViewId: null,
|
|
110
|
+
/** Whether the user chose a view in this session (a late server read then leaves it alone). */
|
|
111
|
+
viewChosen: false,
|
|
112
|
+
/** Id of the view region, the target of the options' `aria-controls`. */
|
|
113
|
+
viewRegionId: `cn-page-view-region-${regionSeq}`,
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Provide the views to descendants, so a greeting header widget in the
|
|
119
|
+
* page's grid can draw the switch. Getters keep it reactive.
|
|
120
|
+
*
|
|
121
|
+
* @return {object} The provided `cnPageViews`.
|
|
122
|
+
*/
|
|
123
|
+
provide() {
|
|
124
|
+
const vm = this;
|
|
125
|
+
return {
|
|
126
|
+
cnPageViews: {
|
|
127
|
+
get views() { return vm.normalizedViews },
|
|
128
|
+
get activeId() { return vm.activeViewId },
|
|
129
|
+
get regionId() { return vm.viewRegionId },
|
|
130
|
+
select: (id) => vm.selectView(id),
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
computed: {
|
|
136
|
+
/**
|
|
137
|
+
* The declared views in one shape, labels translated. Entries without
|
|
138
|
+
* an id or a label are dropped, as are repeated ids.
|
|
139
|
+
*
|
|
140
|
+
* @return {Array<{id: string, label: string, icon: string, widgets: Array<object>, layout: Array<object>, emptyText: string}>}
|
|
141
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
142
|
+
*/
|
|
143
|
+
normalizedViews() {
|
|
144
|
+
const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key;
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
return (Array.isArray(this.views) ? this.views : [])
|
|
147
|
+
.filter((view) => {
|
|
148
|
+
if (!view || typeof view !== 'object' || view.id === undefined || view.id === null || view.id === '') {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
if (typeof view.label !== 'string' || view.label === '' || seen.has(String(view.id))) {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
seen.add(String(view.id));
|
|
155
|
+
return true
|
|
156
|
+
})
|
|
157
|
+
.map((view) => ({
|
|
158
|
+
id: String(view.id),
|
|
159
|
+
label: translate(view.label),
|
|
160
|
+
icon: typeof view.icon === 'string' ? view.icon : '',
|
|
161
|
+
widgets: Array.isArray(view.widgets) ? view.widgets : [],
|
|
162
|
+
layout: Array.isArray(view.layout) ? view.layout : [],
|
|
163
|
+
emptyText: typeof view.emptyText === 'string' && view.emptyText !== '' ? translate(view.emptyText) : '',
|
|
164
|
+
}))
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether this page has views.
|
|
169
|
+
*
|
|
170
|
+
* @return {boolean}
|
|
171
|
+
*/
|
|
172
|
+
hasViews() {
|
|
173
|
+
return this.normalizedViews.length > 0
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The chosen view's id: the address, then this session's or the
|
|
178
|
+
* stored choice, then `defaultView`, then the first view.
|
|
179
|
+
*
|
|
180
|
+
* @return {string|null} The id, or null without views.
|
|
181
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
|
|
182
|
+
*/
|
|
183
|
+
activeViewId() {
|
|
184
|
+
const views = this.normalizedViews;
|
|
185
|
+
if (views.length === 0) {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
const known = (id) => id !== null && id !== undefined && views.some((view) => view.id === String(id));
|
|
189
|
+
const fromAddress = this.$route && this.$route.query ? this.$route.query[PAGE_VIEW_QUERY_KEY] : undefined;
|
|
190
|
+
if (known(fromAddress)) {
|
|
191
|
+
return String(fromAddress)
|
|
192
|
+
}
|
|
193
|
+
if (known(this.selectedViewId)) {
|
|
194
|
+
return String(this.selectedViewId)
|
|
195
|
+
}
|
|
196
|
+
if (known(this.defaultView)) {
|
|
197
|
+
return this.defaultView
|
|
198
|
+
}
|
|
199
|
+
return views[0].id
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The chosen view, or null without views.
|
|
204
|
+
*
|
|
205
|
+
* @return {object|null}
|
|
206
|
+
*/
|
|
207
|
+
activeView() {
|
|
208
|
+
return this.normalizedViews.find((view) => view.id === this.activeViewId) || null
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The switch's options, for CnSegmentedControl.
|
|
213
|
+
*
|
|
214
|
+
* @return {Array<{value: string, label: string}>}
|
|
215
|
+
*/
|
|
216
|
+
viewSwitchOptions() {
|
|
217
|
+
return this.normalizedViews.map((view) => ({ value: view.id, label: view.label }))
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The switch's accessible name: `viewsLabel` translated, else "View".
|
|
222
|
+
*
|
|
223
|
+
* @return {string}
|
|
224
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
225
|
+
*/
|
|
226
|
+
viewSwitchLabel() {
|
|
227
|
+
const translate$1 = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key;
|
|
228
|
+
return this.viewsLabel ? translate$1(this.viewsLabel) : translate('nextcloud-vue', 'Views')
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Whether a greeting header in the page's own widgets draws the
|
|
233
|
+
* switch (an option with a `view`), so the page draws none.
|
|
234
|
+
*
|
|
235
|
+
* @return {boolean}
|
|
236
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
|
|
237
|
+
*/
|
|
238
|
+
viewSwitchInHeaderWidget() {
|
|
239
|
+
const widgets = Array.isArray(this.widgets) ? this.widgets : [];
|
|
240
|
+
return widgets.some((def) => {
|
|
241
|
+
const options = def && def.content && def.content.views && def.content.views.options;
|
|
242
|
+
return Array.isArray(options) && options.some((option) => option && typeof option.view === 'string' && option.view !== '')
|
|
243
|
+
})
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether the page draws the switch itself.
|
|
248
|
+
*
|
|
249
|
+
* @return {boolean}
|
|
250
|
+
*/
|
|
251
|
+
showsPageViewSwitch() {
|
|
252
|
+
return this.hasViews && !this.viewSwitchInHeaderWidget
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The user-preference key for this page's chosen view.
|
|
257
|
+
*
|
|
258
|
+
* @return {string}
|
|
259
|
+
*/
|
|
260
|
+
viewPreferenceKey() {
|
|
261
|
+
// The page id, else the route name: a detail page's fallback id is
|
|
262
|
+
// its record's title, which would remember a view per record.
|
|
263
|
+
const id = this.pageId || (this.$route && this.$route.name) || this.resolvedPageId || 'page';
|
|
264
|
+
return `${PAGE_VIEW_PREFERENCE_PREFIX}${id}`
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The app id the stored view is kept under: the page's `appId` prop
|
|
269
|
+
* when it has one, else the app root's.
|
|
270
|
+
*
|
|
271
|
+
* @return {string}
|
|
272
|
+
*/
|
|
273
|
+
viewPreferenceAppId() {
|
|
274
|
+
const own = typeof this.appId === 'string' ? this.appId : '';
|
|
275
|
+
return own || (typeof this.cnPageViewsAppId === 'string' ? this.cnPageViewsAppId : '')
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The message of the chosen view's empty state.
|
|
280
|
+
*
|
|
281
|
+
* @return {string}
|
|
282
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
283
|
+
*/
|
|
284
|
+
viewEmptyText() {
|
|
285
|
+
if (this.activeView && this.activeView.emptyText) {
|
|
286
|
+
return this.activeView.emptyText
|
|
287
|
+
}
|
|
288
|
+
return translate('nextcloud-vue', 'This view has no widgets yet.')
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
created() {
|
|
293
|
+
this.loadStoredView();
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
methods: {
|
|
297
|
+
/**
|
|
298
|
+
* Read the stored view: the browser mirror at once, then the user
|
|
299
|
+
* preference on the server, which wins unless the user already chose.
|
|
300
|
+
*
|
|
301
|
+
* @return {Promise<void>}
|
|
302
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
|
|
303
|
+
*/
|
|
304
|
+
async loadStoredView() {
|
|
305
|
+
if (!this.hasViews) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
const appId = this.viewPreferenceAppId;
|
|
309
|
+
const key = this.viewPreferenceKey;
|
|
310
|
+
this.selectedViewId = readStoredViewMirror(appId, key);
|
|
311
|
+
if (!appId) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
const stored = await readUserPreference(appId, key, null);
|
|
315
|
+
if (!this.viewChosen && typeof stored === 'string' && stored !== '') {
|
|
316
|
+
this.selectedViewId = stored;
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Choose a view: keep it, store it, and write it to the address
|
|
322
|
+
* (replacing the history entry, keeping the other query keys).
|
|
323
|
+
*
|
|
324
|
+
* @param {string} id The view id.
|
|
325
|
+
* @return {void}
|
|
326
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
|
|
327
|
+
*/
|
|
328
|
+
selectView(id) {
|
|
329
|
+
const view = this.normalizedViews.find((candidate) => candidate.id === String(id));
|
|
330
|
+
if (!view) {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
this.selectedViewId = view.id;
|
|
334
|
+
this.viewChosen = true;
|
|
335
|
+
writeUserPreference(this.viewPreferenceAppId, this.viewPreferenceKey, view.id);
|
|
336
|
+
if (!this.$router || !this.$route) {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
const query = this.$route.query || {};
|
|
340
|
+
if (query[PAGE_VIEW_QUERY_KEY] === view.id) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
const result = this.$router.replace({ query: { ...query, [PAGE_VIEW_QUERY_KEY]: view.id } });
|
|
344
|
+
if (result && typeof result.catch === 'function') {
|
|
345
|
+
result.catch(() => {});
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Look a widget definition up in the views, for a layout item of the
|
|
351
|
+
* chosen view.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} widgetId The widget id.
|
|
354
|
+
* @return {object|null} The definition, or null.
|
|
355
|
+
*/
|
|
356
|
+
findViewWidget(widgetId) {
|
|
357
|
+
const view = this.activeView;
|
|
358
|
+
if (!view) {
|
|
359
|
+
return null
|
|
360
|
+
}
|
|
361
|
+
return view.widgets.find((def) => def && def.id === widgetId) || null
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Write a drag or resize in the chosen view's grid back onto its
|
|
366
|
+
* layout items in place, as the page does for its own layout, so the
|
|
367
|
+
* in-app manifest editor sees it.
|
|
368
|
+
*
|
|
369
|
+
* @param {Array<object>} updated The layout from the grid.
|
|
370
|
+
* @return {void}
|
|
371
|
+
*/
|
|
372
|
+
onViewLayoutChange(updated) {
|
|
373
|
+
const view = this.activeView;
|
|
374
|
+
if (!view || !Array.isArray(updated)) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
for (const u of updated) {
|
|
378
|
+
const item = view.layout.find((l) => String(l.id) === String(u.id));
|
|
379
|
+
if (!item) {
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
for (const key of ['gridX', 'gridY', 'gridWidth', 'gridHeight']) {
|
|
383
|
+
if (u[key] !== undefined) {
|
|
384
|
+
item[key] = u[key];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Emitted when a widget in a view's grid is dragged or resized.
|
|
390
|
+
*
|
|
391
|
+
* @event view-layout-change
|
|
392
|
+
* @type {{view: string, layout: Array<object>}}
|
|
393
|
+
*/
|
|
394
|
+
this.$emit('view-layout-change', { view: view.id, layout: view.layout });
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export { PAGE_VIEW_PREFERENCE_PREFIX, PAGE_VIEW_QUERY_KEY, pageViews };
|
|
400
|
+
//# sourceMappingURL=pageViews.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageViews.js","sources":["../../../src/mixins/pageViews.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Page views: a few views of one page, each its own widget grid, behind one\n * segmented control (\"My work | My team\").\n *\n * A page that mixes this in accepts `views` (each `{ id, label, icon?,\n * widgets, layout, emptyText? }`), `defaultView` and `viewsLabel`. It keeps\n * the chosen view, writes it to the address (`?view=<id>`) and to the\n * user's preferences (the server, mirrored in the browser), and provides `cnPageViews` so a greeting header widget on\n * the page can draw the switch instead of the page.\n *\n * Which view opens: the address, then the stored view, then `defaultView`,\n * then the first view. An id that names no view is ignored at every step.\n *\n * Used by CnDashboardPage and CnDetailPage. Each renders the chosen view's\n * `layout` with its own grid, so a view looks exactly like the page's own\n * grid.\n *\n * @mixin pageViews\n * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { readUserPreference, USER_PREFERENCE_STORAGE_PREFIX, writeUserPreference } from '../composables/useUserPreferences.js'\n\n/** The query parameter that carries the chosen view. */\nexport const PAGE_VIEW_QUERY_KEY = 'view'\n\n/** Prefix of the user-preference key; the page id follows it. */\nexport const PAGE_VIEW_PREFERENCE_PREFIX = 'cn_page_view:'\n\nlet regionSeq = 0\n\n/**\n * Read the browser mirror of a user preference synchronously, so the stored\n * view is on screen from the first frame. Errors read as nothing stored.\n *\n * @param {string} appId The app id.\n * @param {string} key The preference key.\n * @return {string|null} The stored id, or null.\n * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n */\nfunction readStoredViewMirror(appId, key) {\n\ttry {\n\t\tif (typeof localStorage === 'undefined') {\n\t\t\treturn null\n\t\t}\n\t\tconst raw = localStorage.getItem(`${USER_PREFERENCE_STORAGE_PREFIX}${appId}:${key}`)\n\t\tif (raw === null || raw === undefined) {\n\t\t\treturn null\n\t\t}\n\t\tconst value = JSON.parse(raw)\n\t\treturn typeof value === 'string' && value !== '' ? value : null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nexport const pageViews = {\n\tinject: {\n\t\t/** The host app id, provided by CnAppRoot; keys the stored view. */\n\t\tcnPageViewsAppId: { from: 'cnAppId', default: '' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Views of this page, each its own widget grid behind one segmented\n\t\t * control. Each is `{ id, label, icon?, widgets, layout, emptyText? }`:\n\t\t * `widgets` and `layout` have the same shape as the page's own. The\n\t\t * chosen view renders in a region below the page's own grid. Empty\n\t\t * (the default) draws no switch and no region.\n\t\t *\n\t\t * @type {Array<{id: string, label: string, icon?: string, widgets?: Array<object>, layout?: Array<object>, emptyText?: string}>}\n\t\t */\n\t\tviews: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The id of the view chosen when neither the address nor the browser\n\t\t * store names one. Empty chooses the first view.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdefaultView: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch (through the host translate\n\t\t * function). Empty reads \"View\".\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tviewsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\tregionSeq += 1\n\t\treturn {\n\t\t\t/** The view the user chose in this session, or the stored one. */\n\t\t\tselectedViewId: null,\n\t\t\t/** Whether the user chose a view in this session (a late server read then leaves it alone). */\n\t\t\tviewChosen: false,\n\t\t\t/** Id of the view region, the target of the options' `aria-controls`. */\n\t\t\tviewRegionId: `cn-page-view-region-${regionSeq}`,\n\t\t}\n\t},\n\n\t/**\n\t * Provide the views to descendants, so a greeting header widget in the\n\t * page's grid can draw the switch. Getters keep it reactive.\n\t *\n\t * @return {object} The provided `cnPageViews`.\n\t */\n\tprovide() {\n\t\tconst vm = this\n\t\treturn {\n\t\t\tcnPageViews: {\n\t\t\t\tget views() { return vm.normalizedViews },\n\t\t\t\tget activeId() { return vm.activeViewId },\n\t\t\t\tget regionId() { return vm.viewRegionId },\n\t\t\t\tselect: (id) => vm.selectView(id),\n\t\t\t},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The declared views in one shape, labels translated. Entries without\n\t\t * an id or a label are dropped, as are repeated ids.\n\t\t *\n\t\t * @return {Array<{id: string, label: string, icon: string, widgets: Array<object>, layout: Array<object>, emptyText: string}>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n\t\t */\n\t\tnormalizedViews() {\n\t\t\tconst translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\tconst seen = new Set()\n\t\t\treturn (Array.isArray(this.views) ? this.views : [])\n\t\t\t\t.filter((view) => {\n\t\t\t\t\tif (!view || typeof view !== 'object' || view.id === undefined || view.id === null || view.id === '') {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof view.label !== 'string' || view.label === '' || seen.has(String(view.id))) {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tseen.add(String(view.id))\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t\t.map((view) => ({\n\t\t\t\t\tid: String(view.id),\n\t\t\t\t\tlabel: translate(view.label),\n\t\t\t\t\ticon: typeof view.icon === 'string' ? view.icon : '',\n\t\t\t\t\twidgets: Array.isArray(view.widgets) ? view.widgets : [],\n\t\t\t\t\tlayout: Array.isArray(view.layout) ? view.layout : [],\n\t\t\t\t\temptyText: typeof view.emptyText === 'string' && view.emptyText !== '' ? translate(view.emptyText) : '',\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether this page has views.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasViews() {\n\t\t\treturn this.normalizedViews.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The chosen view's id: the address, then this session's or the\n\t\t * stored choice, then `defaultView`, then the first view.\n\t\t *\n\t\t * @return {string|null} The id, or null without views.\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tactiveViewId() {\n\t\t\tconst views = this.normalizedViews\n\t\t\tif (views.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = (id) => id !== null && id !== undefined && views.some((view) => view.id === String(id))\n\t\t\tconst fromAddress = this.$route && this.$route.query ? this.$route.query[PAGE_VIEW_QUERY_KEY] : undefined\n\t\t\tif (known(fromAddress)) {\n\t\t\t\treturn String(fromAddress)\n\t\t\t}\n\t\t\tif (known(this.selectedViewId)) {\n\t\t\t\treturn String(this.selectedViewId)\n\t\t\t}\n\t\t\tif (known(this.defaultView)) {\n\t\t\t\treturn this.defaultView\n\t\t\t}\n\t\t\treturn views[0].id\n\t\t},\n\n\t\t/**\n\t\t * The chosen view, or null without views.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveView() {\n\t\t\treturn this.normalizedViews.find((view) => view.id === this.activeViewId) || null\n\t\t},\n\n\t\t/**\n\t\t * The switch's options, for CnSegmentedControl.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewSwitchOptions() {\n\t\t\treturn this.normalizedViews.map((view) => ({ value: view.id, label: view.label }))\n\t\t},\n\n\t\t/**\n\t\t * The switch's accessible name: `viewsLabel` translated, else \"View\".\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t */\n\t\tviewSwitchLabel() {\n\t\t\tconst translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\treturn this.viewsLabel ? translate(this.viewsLabel) : t('nextcloud-vue', 'Views')\n\t\t},\n\n\t\t/**\n\t\t * Whether a greeting header in the page's own widgets draws the\n\t\t * switch (an option with a `view`), so the page draws none.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views\n\t\t */\n\t\tviewSwitchInHeaderWidget() {\n\t\t\tconst widgets = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn widgets.some((def) => {\n\t\t\t\tconst options = def && def.content && def.content.views && def.content.views.options\n\t\t\t\treturn Array.isArray(options) && options.some((option) => option && typeof option.view === 'string' && option.view !== '')\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the page draws the switch itself.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsPageViewSwitch() {\n\t\t\treturn this.hasViews && !this.viewSwitchInHeaderWidget\n\t\t},\n\n\t\t/**\n\t\t * The user-preference key for this page's chosen view.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewPreferenceKey() {\n\t\t\t// The page id, else the route name: a detail page's fallback id is\n\t\t\t// its record's title, which would remember a view per record.\n\t\t\tconst id = this.pageId || (this.$route && this.$route.name) || this.resolvedPageId || 'page'\n\t\t\treturn `${PAGE_VIEW_PREFERENCE_PREFIX}${id}`\n\t\t},\n\n\t\t/**\n\t\t * The app id the stored view is kept under: the page's `appId` prop\n\t\t * when it has one, else the app root's.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewPreferenceAppId() {\n\t\t\tconst own = typeof this.appId === 'string' ? this.appId : ''\n\t\t\treturn own || (typeof this.cnPageViewsAppId === 'string' ? this.cnPageViewsAppId : '')\n\t\t},\n\n\t\t/**\n\t\t * The message of the chosen view's empty state.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewEmptyText() {\n\t\t\tif (this.activeView && this.activeView.emptyText) {\n\t\t\t\treturn this.activeView.emptyText\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'This view has no widgets yet.')\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadStoredView()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the stored view: the browser mirror at once, then the user\n\t\t * preference on the server, which wins unless the user already chose.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tasync loadStoredView() {\n\t\t\tif (!this.hasViews) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst appId = this.viewPreferenceAppId\n\t\t\tconst key = this.viewPreferenceKey\n\t\t\tthis.selectedViewId = readStoredViewMirror(appId, key)\n\t\t\tif (!appId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stored = await readUserPreference(appId, key, null)\n\t\t\tif (!this.viewChosen && typeof stored === 'string' && stored !== '') {\n\t\t\t\tthis.selectedViewId = stored\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Choose a view: keep it, store it, and write it to the address\n\t\t * (replacing the history entry, keeping the other query keys).\n\t\t *\n\t\t * @param {string} id The view id.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tselectView(id) {\n\t\t\tconst view = this.normalizedViews.find((candidate) => candidate.id === String(id))\n\t\t\tif (!view) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.selectedViewId = view.id\n\t\t\tthis.viewChosen = true\n\t\t\twriteUserPreference(this.viewPreferenceAppId, this.viewPreferenceKey, view.id)\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst query = this.$route.query || {}\n\t\t\tif (query[PAGE_VIEW_QUERY_KEY] === view.id) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = this.$router.replace({ query: { ...query, [PAGE_VIEW_QUERY_KEY]: view.id } })\n\t\t\tif (result && typeof result.catch === 'function') {\n\t\t\t\tresult.catch(() => {})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Look a widget definition up in the views, for a layout item of the\n\t\t * chosen view.\n\t\t *\n\t\t * @param {string} widgetId The widget id.\n\t\t * @return {object|null} The definition, or null.\n\t\t */\n\t\tfindViewWidget(widgetId) {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn view.widgets.find((def) => def && def.id === widgetId) || null\n\t\t},\n\n\t\t/**\n\t\t * Write a drag or resize in the chosen view's grid back onto its\n\t\t * layout items in place, as the page does for its own layout, so the\n\t\t * in-app manifest editor sees it.\n\t\t *\n\t\t * @param {Array<object>} updated The layout from the grid.\n\t\t * @return {void}\n\t\t */\n\t\tonViewLayoutChange(updated) {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view || !Array.isArray(updated)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor (const u of updated) {\n\t\t\t\tconst item = view.layout.find((l) => String(l.id) === String(u.id))\n\t\t\t\tif (!item) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor (const key of ['gridX', 'gridY', 'gridWidth', 'gridHeight']) {\n\t\t\t\t\tif (u[key] !== undefined) {\n\t\t\t\t\t\titem[key] = u[key]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * Emitted when a widget in a view's grid is dragged or resized.\n\t\t\t *\n\t\t\t * @event view-layout-change\n\t\t\t * @type {{view: string, layout: Array<object>}}\n\t\t\t */\n\t\t\tthis.$emit('view-layout-change', { view: view.id, layout: view.layout })\n\t\t},\n\t},\n}\n"],"names":["translate","t"],"mappings":"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"}
|
|
@@ -1283,6 +1283,53 @@
|
|
|
1283
1283
|
font-size: var(--cn-kpi-value-size-stacked, 2rem);
|
|
1284
1284
|
}
|
|
1285
1285
|
|
|
1286
|
+
/* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
|
|
1287
|
+
label, the number at 34px/700 in the text colour, and the caption on a line
|
|
1288
|
+
of its own; CnStatWidget draws no icon circle in this layout. The padding
|
|
1289
|
+
adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
|
|
1290
|
+
Every value is a token so a theme can move it. */
|
|
1291
|
+
.cn-kpi-card--stacked {
|
|
1292
|
+
flex-direction: column;
|
|
1293
|
+
align-items: stretch;
|
|
1294
|
+
justify-content: center;
|
|
1295
|
+
gap: 0;
|
|
1296
|
+
padding: var(--cn-kpi-stacked-padding, 10px 6px);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.cn-kpi-card--stacked .cn-kpi-card__body {
|
|
1300
|
+
gap: var(--cn-kpi-stacked-gap, 6px);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.cn-kpi-card--stacked .cn-kpi-card__title {
|
|
1304
|
+
font-size: var(--cn-kpi-stacked-title-size, 14px);
|
|
1305
|
+
font-weight: var(--cn-kpi-stacked-title-weight, 400);
|
|
1306
|
+
color: var(--color-text-maxcontrast);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.cn-kpi-card--stacked .cn-kpi-card__value {
|
|
1310
|
+
font-size: var(--cn-kpi-stacked-value-size, 34px);
|
|
1311
|
+
font-weight: var(--cn-kpi-value-weight, 700);
|
|
1312
|
+
line-height: 1;
|
|
1313
|
+
letter-spacing: -0.01em;
|
|
1314
|
+
color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
.cn-kpi-card--stacked .cn-kpi-card__caption-line {
|
|
1318
|
+
flex: 0 0 auto;
|
|
1319
|
+
max-width: 100%;
|
|
1320
|
+
font-size: var(--cn-kpi-label-size, 13px);
|
|
1321
|
+
font-weight: 500;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/* A linked stacked tile reads as a tile, not as underlined link text. The
|
|
1325
|
+
NL Design theme underlines every `a` with `!important`, so the opt-out
|
|
1326
|
+
needs it too; it only applies to this opt-in layout. */
|
|
1327
|
+
a.cn-kpi-card--stacked,
|
|
1328
|
+
a.cn-kpi-card--stacked:hover,
|
|
1329
|
+
a.cn-kpi-card--stacked:focus-visible {
|
|
1330
|
+
text-decoration: none !important;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1286
1333
|
/* Filled: the card draws its own grey box, for a KPI that is NOT already
|
|
1287
1334
|
inside a wrapper that draws one. Opt-in via `content.flat: false`. */
|
|
1288
1335
|
.cn-kpi-card--filled {
|
|
@@ -2790,6 +2837,28 @@
|
|
|
2790
2837
|
min-width: 0;
|
|
2791
2838
|
}
|
|
2792
2839
|
|
|
2840
|
+
/* A widget in the header (`config.headerWidget`) takes a row of its own under
|
|
2841
|
+
the title and the actions. */
|
|
2842
|
+
.cn-detail-page__header--with-widget {
|
|
2843
|
+
flex-wrap: wrap;
|
|
2844
|
+
row-gap: var(--cn-detail-header-row-gap, 22px);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
.cn-detail-page__header-widget {
|
|
2848
|
+
flex: 1 0 100%;
|
|
2849
|
+
min-width: 0;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/* The header as a card (`config.headerCard`): the board's case header, a
|
|
2853
|
+
bordered box on the surface colour. The 56px inset that clears the
|
|
2854
|
+
navigation toggle is not needed inside a card that starts below it. */
|
|
2855
|
+
.cn-detail-page__header--card {
|
|
2856
|
+
padding: var(--cn-detail-header-card-padding, 22px 24px);
|
|
2857
|
+
border: 1px solid var(--color-border);
|
|
2858
|
+
border-radius: var(--cn-detail-header-card-radius, 12px);
|
|
2859
|
+
background: var(--color-main-background);
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2793
2862
|
.cn-detail-page__header-left {
|
|
2794
2863
|
display: flex;
|
|
2795
2864
|
align-items: center;
|
|
@@ -3210,6 +3279,11 @@
|
|
|
3210
3279
|
font-size: 0.9em;
|
|
3211
3280
|
white-space: nowrap;
|
|
3212
3281
|
}
|
|
3282
|
+
|
|
3283
|
+
/* A page view the switch shows with nothing to draw (view-switch-containers). */
|
|
3284
|
+
.cn-detail-page .cn-page-view-region--empty {
|
|
3285
|
+
padding: 40px 20px;
|
|
3286
|
+
}
|
|
3213
3287
|
/* CnReferencePreview — a reference that shows what it points at. */
|
|
3214
3288
|
|
|
3215
3289
|
.cn-reference-preview {
|
|
@@ -16875,7 +16949,17 @@ a.cn-xwiki-card__title[data-v-54e85fe3]:hover {
|
|
|
16875
16949
|
text-overflow: ellipsis;
|
|
16876
16950
|
white-space: nowrap;
|
|
16877
16951
|
}
|
|
16952
|
+
|
|
16953
|
+
/* A clickable label is a <button>, and Nextcloud's core stylesheet gives
|
|
16954
|
+
every `button:not(.button-vue)` a min-height of the clickable area (34 to
|
|
16955
|
+
44px) with the text centred in it. That pushed each reachable stage's label
|
|
16956
|
+
about 12px below the current one, which is a <span>. Zeroed here so all
|
|
16957
|
+
labels sit 8px under their bar, as the board draws them. The visually
|
|
16958
|
+
hidden "(current step)" is absolutely positioned and takes no space. */
|
|
16878
16959
|
button.cn-stages-widget__bar-label[data-v-358fde0f] {
|
|
16960
|
+
min-height: 0;
|
|
16961
|
+
height: auto;
|
|
16962
|
+
line-height: inherit;
|
|
16879
16963
|
cursor: pointer;
|
|
16880
16964
|
}
|
|
16881
16965
|
button.cn-stages-widget__bar-label[aria-disabled="true"][data-v-358fde0f] {
|
|
@@ -17310,6 +17394,53 @@ button.cn-stages-widget__bar-label[data-v-358fde0f]:focus-visible {
|
|
|
17310
17394
|
font-size: var(--cn-kpi-value-size-stacked, 2rem);
|
|
17311
17395
|
}
|
|
17312
17396
|
|
|
17397
|
+
/* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
|
|
17398
|
+
label, the number at 34px/700 in the text colour, and the caption on a line
|
|
17399
|
+
of its own; CnStatWidget draws no icon circle in this layout. The padding
|
|
17400
|
+
adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
|
|
17401
|
+
Every value is a token so a theme can move it. */
|
|
17402
|
+
.cn-kpi-card--stacked {
|
|
17403
|
+
flex-direction: column;
|
|
17404
|
+
align-items: stretch;
|
|
17405
|
+
justify-content: center;
|
|
17406
|
+
gap: 0;
|
|
17407
|
+
padding: var(--cn-kpi-stacked-padding, 10px 6px);
|
|
17408
|
+
}
|
|
17409
|
+
|
|
17410
|
+
.cn-kpi-card--stacked .cn-kpi-card__body {
|
|
17411
|
+
gap: var(--cn-kpi-stacked-gap, 6px);
|
|
17412
|
+
}
|
|
17413
|
+
|
|
17414
|
+
.cn-kpi-card--stacked .cn-kpi-card__title {
|
|
17415
|
+
font-size: var(--cn-kpi-stacked-title-size, 14px);
|
|
17416
|
+
font-weight: var(--cn-kpi-stacked-title-weight, 400);
|
|
17417
|
+
color: var(--color-text-maxcontrast);
|
|
17418
|
+
}
|
|
17419
|
+
|
|
17420
|
+
.cn-kpi-card--stacked .cn-kpi-card__value {
|
|
17421
|
+
font-size: var(--cn-kpi-stacked-value-size, 34px);
|
|
17422
|
+
font-weight: var(--cn-kpi-value-weight, 700);
|
|
17423
|
+
line-height: 1;
|
|
17424
|
+
letter-spacing: -0.01em;
|
|
17425
|
+
color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
|
|
17426
|
+
}
|
|
17427
|
+
|
|
17428
|
+
.cn-kpi-card--stacked .cn-kpi-card__caption-line {
|
|
17429
|
+
flex: 0 0 auto;
|
|
17430
|
+
max-width: 100%;
|
|
17431
|
+
font-size: var(--cn-kpi-label-size, 13px);
|
|
17432
|
+
font-weight: 500;
|
|
17433
|
+
}
|
|
17434
|
+
|
|
17435
|
+
/* A linked stacked tile reads as a tile, not as underlined link text. The
|
|
17436
|
+
NL Design theme underlines every `a` with `!important`, so the opt-out
|
|
17437
|
+
needs it too; it only applies to this opt-in layout. */
|
|
17438
|
+
a.cn-kpi-card--stacked,
|
|
17439
|
+
a.cn-kpi-card--stacked:hover,
|
|
17440
|
+
a.cn-kpi-card--stacked:focus-visible {
|
|
17441
|
+
text-decoration: none !important;
|
|
17442
|
+
}
|
|
17443
|
+
|
|
17313
17444
|
/* Filled: the card draws its own grey box, for a KPI that is NOT already
|
|
17314
17445
|
inside a wrapper that draws one. Opt-in via `content.flat: false`. */
|
|
17315
17446
|
.cn-kpi-card--filled {
|
|
@@ -18262,6 +18393,16 @@ to { background-position: -200% 0;
|
|
|
18262
18393
|
flex: 0 0 auto;
|
|
18263
18394
|
padding: 16px;
|
|
18264
18395
|
}
|
|
18396
|
+
|
|
18397
|
+
/* On the page ground (`content.ground`): no card, so no inset around the
|
|
18398
|
+
view switch, and the heading at the board's 32px. */
|
|
18399
|
+
.cn-header-widget--ground .cn-header-widget__views[data-v-1b97430f] {
|
|
18400
|
+
padding: 0;
|
|
18401
|
+
}
|
|
18402
|
+
.cn-header-widget--ground .cn-header-widget__title[data-v-1b97430f] {
|
|
18403
|
+
font-size: var(--cn-header-ground-title-size, 32px);
|
|
18404
|
+
letter-spacing: -0.01em;
|
|
18405
|
+
}
|
|
18265
18406
|
.cn-header-widget__date[data-v-1b97430f] {
|
|
18266
18407
|
font-size: 14px;
|
|
18267
18408
|
line-height: 1.4;
|
|
@@ -30431,6 +30572,19 @@ to {
|
|
|
30431
30572
|
.cn-dashboard-page__empty[data-v-824052fe] {
|
|
30432
30573
|
padding: 60px 20px;
|
|
30433
30574
|
}
|
|
30575
|
+
|
|
30576
|
+
/* The page's view switch on a page without a header row: at the end of its
|
|
30577
|
+
own row, above the grids, where the header would have put it. */
|
|
30578
|
+
.cn-dashboard-page__view-switch[data-v-824052fe] {
|
|
30579
|
+
display: flex;
|
|
30580
|
+
justify-content: flex-end;
|
|
30581
|
+
margin-bottom: 12px;
|
|
30582
|
+
}
|
|
30583
|
+
|
|
30584
|
+
/* A view the switch shows with nothing to draw. */
|
|
30585
|
+
.cn-page-view-region--empty[data-v-824052fe] {
|
|
30586
|
+
padding: 40px 20px;
|
|
30587
|
+
}
|
|
30434
30588
|
.cn-dashboard-page__page-filters[data-v-824052fe] {
|
|
30435
30589
|
display: flex;
|
|
30436
30590
|
flex-wrap: wrap;
|
|
@@ -33247,6 +33401,21 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
|
|
|
33247
33401
|
padding: calc(var(--default-grid-baseline, 4px) * 2);
|
|
33248
33402
|
}
|
|
33249
33403
|
|
|
33404
|
+
/*
|
|
33405
|
+
* The primary action never shrinks out of sight. NcAppNavigation renders it in
|
|
33406
|
+
* `.app-navigation__body`, a flex child with `overflow-y: scroll` (so its
|
|
33407
|
+
* minimum height is 0) beside a list of `height: 100%` and the footer. With a
|
|
33408
|
+
* card and footer entries below the list, the column overflows and the body
|
|
33409
|
+
* gives up its share of the shrink: "New case" showed as a 24px sliver of a
|
|
33410
|
+
* 42px button. All three forms carry `.app-navigation-new` (the solid and
|
|
33411
|
+
* link wrappers mirror NcAppNavigationNew's class), and the rule is scoped to
|
|
33412
|
+
* this component's navigation, so a navigation without a primary action lays
|
|
33413
|
+
* out exactly as before.
|
|
33414
|
+
*/
|
|
33415
|
+
.app-navigation[data-testid="cn-nav"] .app-navigation__body:has(> .app-navigation-new) {
|
|
33416
|
+
flex-shrink: 0;
|
|
33417
|
+
}
|
|
33418
|
+
|
|
33250
33419
|
/*
|
|
33251
33420
|
* Footer-section items (section: "footer") render in NcAppNavigation's
|
|
33252
33421
|
* #footer slot, outside the scroll container, so they stay visible above
|
|
@@ -33270,6 +33439,19 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
|
|
|
33270
33439
|
overflow: visible !important;
|
|
33271
33440
|
}
|
|
33272
33441
|
|
|
33442
|
+
/*
|
|
33443
|
+
* A declared footer that names `settings` first (`nav.footer`) puts the
|
|
33444
|
+
* foldout above the footer list. The two are siblings in NcAppNavigation's
|
|
33445
|
+
* flex column, so `order` moves them without changing either's markup; the
|
|
33446
|
+
* card above keeps order 0 and stays first.
|
|
33447
|
+
*/
|
|
33448
|
+
.cn-app-nav__settings--first {
|
|
33449
|
+
order: 1;
|
|
33450
|
+
}
|
|
33451
|
+
.cn-app-nav__footer-list--after-settings {
|
|
33452
|
+
order: 2;
|
|
33453
|
+
}
|
|
33454
|
+
|
|
33273
33455
|
/*
|
|
33274
33456
|
* Align the settings foldout's items with the main/footer list items,
|
|
33275
33457
|
* whose icons sit at a 16px inset. The foldout's items live in a bare
|