@conduction/nextcloud-vue 2.65.0 → 2.67.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/CnActionsBar/CnActionsBar.vue2.js +19 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +2 -2
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
- 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/CnDataTable/CnDataTable.vue2.js +4 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +78 -21
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
- 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 +63 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +335 -313
- package/dist/esm/components/CnIndexPage/CnIndexPage.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/CnSetupWizard/CnSetupWizard.vue2.js +5 -11
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +44 -1
- package/dist/esm/composables/useDependencyCheck.js.map +1 -1
- package/dist/esm/composables/useListView.js +43 -1
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useSetupStatus.js +26 -3
- package/dist/esm/composables/useSetupStatus.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -1
- 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 +38 -2
- package/dist/esm/store/plugins/liveUpdates.js +19 -9
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +20 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +11 -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 +2377 -1247
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +38 -2
- package/l10n/en.json +7 -1
- package/l10n/nl.json +7 -1
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +2 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
- package/src/components/CnAppNav/CnAppNav.vue +2 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +165 -8
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +4 -4
- package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
- package/src/components/CnIndexPage/CnIndexPage.md +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +5 -11
- package/src/composables/useDependencyCheck.js +43 -0
- package/src/composables/useListView.js +43 -1
- package/src/composables/useSetupStatus.js +26 -3
- package/src/css/actions-bar.css +20 -2
- package/src/css/detail-page.css +5 -0
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +31 -3
- package/src/schemas/app-manifest.schema.json +1 -1
- package/src/store/plugins/liveUpdates.js +19 -9
- package/src/store/useObjectStore.js +19 -8
- package/src/utils/actionsDispatcher.js +11 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -9,6 +9,9 @@ const ALLOWED_TEXT_ALIGN = ['left', 'center', 'right'];
|
|
|
9
9
|
const ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom'];
|
|
10
10
|
const ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost'];
|
|
11
11
|
|
|
12
|
+
/** Marks a switch option that selects a page view rather than a route. */
|
|
13
|
+
const VIEW_OPTION_PREFIX = 'view:';
|
|
14
|
+
|
|
12
15
|
const HEIGHT_PIXELS = Object.freeze({
|
|
13
16
|
small: 120,
|
|
14
17
|
medium: 200,
|
|
@@ -64,6 +67,12 @@ var script = {
|
|
|
64
67
|
* untranslated key renders as itself.
|
|
65
68
|
*/
|
|
66
69
|
cnTranslate: { default: () => (key) => key },
|
|
70
|
+
/**
|
|
71
|
+
* The views of the page this widget sits on, provided by
|
|
72
|
+
* CnDashboardPage / CnDetailPage (`views`). An option with a `view`
|
|
73
|
+
* selects one of them. Null outside such a page.
|
|
74
|
+
*/
|
|
75
|
+
cnPageViews: { default: null },
|
|
67
76
|
},
|
|
68
77
|
|
|
69
78
|
props: {
|
|
@@ -74,9 +83,12 @@ var script = {
|
|
|
74
83
|
* greeting, showDate, plain, ground, views}`. All fields are optional except
|
|
75
84
|
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
76
85
|
* defaults and the renderer never throws. `views` is
|
|
77
|
-
* `{ ariaLabel?, options: [{ label, route, params? }] }`:
|
|
78
|
-
* control at the right of the heading whose checked option
|
|
79
|
-
* current route; choosing another pushes its route.
|
|
86
|
+
* `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:
|
|
87
|
+
* a segmented control at the right of the heading whose checked option
|
|
88
|
+
* is the current route; choosing another pushes its route. An option
|
|
89
|
+
* with `view` selects that view of the page the widget sits on (the
|
|
90
|
+
* page's `views`) instead, and the page then draws no switch of its
|
|
91
|
+
* own. `ground: true`
|
|
80
92
|
* draws the greeting on the page ground: the plain look with no
|
|
81
93
|
* padding, a 32px heading and the date line 6px above it (a dashboard
|
|
82
94
|
* also drops the widget's card for it).
|
|
@@ -172,12 +184,45 @@ var script = {
|
|
|
172
184
|
*/
|
|
173
185
|
viewOptions() {
|
|
174
186
|
const raw = this.content && this.content.views && this.content.views.options;
|
|
175
|
-
if (!Array.isArray(raw)
|
|
187
|
+
if (!Array.isArray(raw)) {
|
|
176
188
|
return []
|
|
177
189
|
}
|
|
190
|
+
const pageViewIds = this.pageViewIds;
|
|
178
191
|
return raw
|
|
179
|
-
.filter((o) => o && typeof o.label === 'string' && o.label !== ''
|
|
180
|
-
.map((o) =>
|
|
192
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '')
|
|
193
|
+
.map((o) => {
|
|
194
|
+
if (typeof o.view === 'string' && o.view !== '') {
|
|
195
|
+
return pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null
|
|
196
|
+
}
|
|
197
|
+
if (typeof o.route === 'string' && o.route !== '' && this.$router) {
|
|
198
|
+
return { value: o.route, label: this.cnTranslate(o.label) }
|
|
199
|
+
}
|
|
200
|
+
return null
|
|
201
|
+
})
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The ids of the page's views, empty outside a page with views.
|
|
207
|
+
*
|
|
208
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
|
|
209
|
+
* @return {string[]}
|
|
210
|
+
*/
|
|
211
|
+
pageViewIds() {
|
|
212
|
+
const views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : [];
|
|
213
|
+
return views.map((view) => view.id)
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The id of the page's view region when an option selects a view,
|
|
218
|
+
* for the options' `aria-controls`; '' otherwise.
|
|
219
|
+
*
|
|
220
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
221
|
+
* @return {string}
|
|
222
|
+
*/
|
|
223
|
+
viewsRegionId() {
|
|
224
|
+
const hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX));
|
|
225
|
+
return hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''
|
|
181
226
|
},
|
|
182
227
|
|
|
183
228
|
/**
|
|
@@ -192,6 +237,11 @@ var script = {
|
|
|
192
237
|
if (options.length === 0) {
|
|
193
238
|
return null
|
|
194
239
|
}
|
|
240
|
+
const activeView = this.cnPageViews ? this.cnPageViews.activeId : null;
|
|
241
|
+
const viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null;
|
|
242
|
+
if (viewMatch) {
|
|
243
|
+
return viewMatch.value
|
|
244
|
+
}
|
|
195
245
|
const current = this.$route && this.$route.name;
|
|
196
246
|
const match = options.find((o) => o.value === current);
|
|
197
247
|
return match ? match.value : options[0].value
|
|
@@ -206,7 +256,7 @@ var script = {
|
|
|
206
256
|
*/
|
|
207
257
|
viewsLabel() {
|
|
208
258
|
const label = this.content && this.content.views && this.content.views.ariaLabel;
|
|
209
|
-
return label ? this.cnTranslate(label) : translate('nextcloud-vue', '
|
|
259
|
+
return label ? this.cnTranslate(label) : translate('nextcloud-vue', 'Views')
|
|
210
260
|
},
|
|
211
261
|
|
|
212
262
|
/**
|
|
@@ -640,6 +690,12 @@ var script = {
|
|
|
640
690
|
* @return {void}
|
|
641
691
|
*/
|
|
642
692
|
onViewChange(route) {
|
|
693
|
+
if (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {
|
|
694
|
+
if (this.cnPageViews && typeof this.cnPageViews.select === 'function') {
|
|
695
|
+
this.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length));
|
|
696
|
+
}
|
|
697
|
+
return
|
|
698
|
+
}
|
|
643
699
|
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
644
700
|
return
|
|
645
701
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnHeaderWidget.vue2.js","sources":["../../../../src/components/CnHeaderWidget/CnHeaderWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<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\">\n\t\t<div\n\t\t\tv-if=\"hasOverlay\"\n\t\t\tclass=\"cn-header-widget__overlay\"\n\t\t\t:style=\"overlayStyle\"\n\t\t\taria-hidden=\"true\" />\n\t\t<div class=\"cn-header-widget__content\" :style=\"contentStyle\">\n\t\t\t<p\n\t\t\t\tv-if=\"dateLine\"\n\t\t\t\tclass=\"cn-header-widget__date\"\n\t\t\t\t:style=\"dateStyle\"\n\t\t\t\tdata-testid=\"cn-header-widget-date\">\n\t\t\t\t{{ dateLine }}\n\t\t\t</p>\n\t\t\t<h2 v-if=\"hasTitle\" class=\"cn-header-widget__title\" :style=\"textStyle\">\n\t\t\t\t{{ headingText }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"hasSubtitle\" class=\"cn-header-widget__subtitle\" :style=\"textStyle\">\n\t\t\t\t{{ subtitle }}\n\t\t\t</p>\n\t\t\t<a\n\t\t\t\tv-if=\"hasCta\"\n\t\t\t\t:href=\"ctaUrl\"\n\t\t\t\t:class=\"ctaClasses\"\n\t\t\t\t:target=\"ctaTarget\"\n\t\t\t\t:rel=\"ctaRel\"\n\t\t\t\t:aria-label=\"ctaAriaLabel\">\n\t\t\t\t{{ ctaLabel }}\n\t\t\t</a>\n\t\t</div>\n\t\t<!-- A view switch at the right of the heading (`content.views`): \"My\n\t\t work / My team\" as a segmented control whose options are routes. -->\n\t\t<div v-if=\"viewOptions.length > 0\" class=\"cn-header-widget__views\">\n\t\t\t<CnSegmentedControl\n\t\t\t\t:options=\"viewOptions\"\n\t\t\t\t:modelValue=\"activeView\"\n\t\t\t\t:ariaLabel=\"viewsLabel\"\n\t\t\t\tdata-testid=\"cn-header-widget-views\"\n\t\t\t\t@update:modelValue=\"onViewChange\" />\n\t\t</div>\n\t\t<img\n\t\t\tv-if=\"hasBackgroundImage\"\n\t\t\tclass=\"cn-header-widget__probe\"\n\t\t\t:src=\"backgroundImageUrl\"\n\t\t\talt=\"\"\n\t\t\taria-hidden=\"true\"\n\t\t\t@error=\"onImageError\">\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { getCanonicalLocale, translate as t } from '@nextcloud/l10n'\nimport CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'\nimport { resolveImageUrl } from '../../utils/resolveImageUrl.js'\n\nconst ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']\nconst ALLOWED_HEIGHTS = ['small', 'medium', 'large', 'xlarge']\nconst ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']\nconst ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']\nconst ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']\n\nconst HEIGHT_PIXELS = Object.freeze({\n\tsmall: 120,\n\tmedium: 200,\n\tlarge: 320,\n\txlarge: 480,\n})\n\nconst VERTICAL_ALIGN_FLEX = Object.freeze({\n\ttop: 'flex-start',\n\tmiddle: 'center',\n\tbottom: 'flex-end',\n})\n\n/**\n * CnHeaderWidget — full-width banner widget for dashboards. Renders a\n * configurable header with title, optional subtitle, optional background\n * image, optional colour overlay, and optional call-to-action button.\n *\n * Image source precedence: `backgroundImageFileId` takes priority over\n * `backgroundImageUrl` (resolved via the Nextcloud core preview route). When\n * the image fails to load the renderer silently falls back to the solid\n * `backgroundColor`; no error UI is shown.\n *\n * Height presets map to fixed pixels: small=120, medium=200, large=320,\n * xlarge=480. Title is `<h2>`, subtitle `<p>`, CTA `<a>` — all semantic HTML.\n * External CTA URLs (http/https) open in a new tab with\n * `rel=\"noopener noreferrer\"`; relative / anchor URLs stay in the current tab.\n *\n * Optional greeting: `greeting: true` makes the heading greet the signed-in\n * user by time of day and first name (\"Good afternoon, Pieter\"; `\"full\"` uses\n * the whole display name), and `showDate: true` adds a line with today's\n * date above it. `plain: true` drops the coloured background and aligns the\n * text to the start, which is how a greeting usually sits on a dashboard.\n *\n * ```json\n * { \"widgetKey\": \"header\", \"props\": { \"content\": { \"greeting\": true, \"showDate\": true, \"plain\": true } } }\n * ```\n *\n * Registered as the `header` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnHeaderWidget',\n\n\tcomponents: { CnSegmentedControl },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.title` / `content.subtitle` / CTA label\n\t\t * are run through it. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content: `{title, subtitle, backgroundImageUrl,\n\t\t * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,\n\t\t * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,\n\t\t * greeting, showDate, plain, ground, views}`. All fields are optional except\n\t\t * `title` (or `greeting`); unknown enum values collapse to documented\n\t\t * defaults and the renderer never throws. `views` is\n\t\t * `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented\n\t\t * control at the right of the heading whose checked option is the\n\t\t * current route; choosing another pushes its route. `ground: true`\n\t\t * draws the greeting on the page ground: the plain look with no\n\t\t * padding, a 32px heading and the date line 6px above it (a dashboard\n\t\t * also drops the widget's card for it).\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/* eslint-disable vue/no-unused-properties -- part of the widget contract: CnContainerChild and the dashboard pages bind :placement on every widget, so declaring it keeps it out of $attrs */\n\t\t/**\n\t\t * Placement entity — reserved to match the renderer contract.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/**\n\t\t * The moment the greeting and the date line are computed for. Leave\n\t\t * empty for the current time.\n\t\t *\n\t\t * @type {Date|null}\n\t\t */\n\t\tnow: {\n\t\t\ttype: Date,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t// Set true once the background image fires a DOM `error` so\n\t\t\t// subsequent renders fall back to `backgroundColor` only. The\n\t\t\t// probe `<img>` is invisible — its only job is load-failure\n\t\t\t// detection for the CSS background.\n\t\t\timageFailed: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the injected `cnTranslate` (the host\n\t\t * app's bound `t()`), identity by default.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t},\n\n\t\t/** The header title, translated, or '' when absent. */\n\t\ttitle() {\n\t\t\tconst value = this.content && this.content.title\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** The header subtitle, translated, or '' when absent. */\n\t\tsubtitle() {\n\t\t\tconst value = this.content && this.content.subtitle\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** Whether a heading renders: a non-empty title or a greeting. */\n\t\thasTitle() {\n\t\t\treturn this.headingText !== ''\n\t\t},\n\n\t\t/**\n\t\t * The moment the greeting and date line are computed for.\n\t\t *\n\t\t * @return {Date}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\treference() {\n\t\t\treturn this.now instanceof Date ? this.now : new Date()\n\t\t},\n\n\t\t/**\n\t\t * The view switch's options from `content.views.options`: each\n\t\t * `{ label, route, params? }` with a non-empty label and route becomes\n\t\t * a segmented-control option keyed on its route name (the label goes\n\t\t * through the host translate function). Empty without a router, so\n\t\t * the switch cannot offer routes it cannot follow.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewOptions() {\n\t\t\tconst raw = this.content && this.content.views && this.content.views.options\n\t\t\tif (!Array.isArray(raw) || !this.$router) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn raw\n\t\t\t\t.filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')\n\t\t\t\t.map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))\n\t\t},\n\n\t\t/**\n\t\t * The checked view: the option whose route is the current route, else\n\t\t * the first option (a switch always has one checked option).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string|null}\n\t\t */\n\t\tactiveView() {\n\t\t\tconst options = this.viewOptions\n\t\t\tif (options.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst current = this.$route && this.$route.name\n\t\t\tconst match = options.find((o) => o.value === current)\n\t\t\treturn match ? match.value : options[0].value\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch: `content.views.ariaLabel`\n\t\t * through the host translate function, else \"View\".\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string}\n\t\t */\n\t\tviewsLabel() {\n\t\t\tconst label = this.content && this.content.views && this.content.views.ariaLabel\n\t\t\treturn label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')\n\t\t},\n\n\t\t/**\n\t\t * Whether the plain presentation is on (no coloured background).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisPlain() {\n\t\t\treturn Boolean(this.content && (this.content.plain === true || this.content.ground === true))\n\t\t},\n\n\t\t/**\n\t\t * Whether the greeting sits on the page ground (`content.ground`):\n\t\t * the plain look without the card padding, so the date line and the\n\t\t * heading align with the page edge as the board draws them. Off by\n\t\t * default.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground\n\t\t * @return {boolean}\n\t\t */\n\t\tisGround() {\n\t\t\treturn Boolean(this.content && this.content.ground === true)\n\t\t},\n\n\t\t/**\n\t\t * The name the greeting uses: the first word of the display name, or\n\t\t * the whole display name for `greeting: \"full\"`. '' when unknown.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingName() {\n\t\t\tlet user\n\t\t\ttry {\n\t\t\t\tuser = getCurrentUser()\n\t\t\t} catch {\n\t\t\t\tuser = null\n\t\t\t}\n\t\t\tconst displayName = (user && typeof user.displayName === 'string') ? user.displayName.trim() : ''\n\t\t\tif (displayName === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.content.greeting === 'full' ? displayName : displayName.split(/\\s+/)[0]\n\t\t},\n\n\t\t/**\n\t\t * The greeting for the time of day, with the user's name when known,\n\t\t * or '' when the greeting is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingText() {\n\t\t\tconst greeting = this.content && this.content.greeting\n\t\t\tif (greeting !== true && greeting !== 'full' && greeting !== 'first') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst hour = this.reference.getHours()\n\t\t\tconst name = this.greetingName\n\t\t\tif (hour < 12) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good morning, {name}', { name }) : t('nextcloud-vue', 'Good morning')\n\t\t\t}\n\t\t\tif (hour < 18) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good afternoon, {name}', { name }) : t('nextcloud-vue', 'Good afternoon')\n\t\t\t}\n\t\t\treturn name ? t('nextcloud-vue', 'Good evening, {name}', { name }) : t('nextcloud-vue', 'Good evening')\n\t\t},\n\n\t\t/**\n\t\t * The heading: the greeting when it is on, else the title.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\theadingText() {\n\t\t\treturn this.greetingText || this.title\n\t\t},\n\n\t\t/**\n\t\t * Today's date written out (\"Monday 5 October 2026\"), or '' when\n\t\t * `showDate` is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateLine() {\n\t\t\tif (!this.content || this.content.showDate !== true) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }\n\t\t\tlet locale\n\t\t\ttry {\n\t\t\t\tlocale = getCanonicalLocale()\n\t\t\t} catch {\n\t\t\t\tlocale = undefined\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale || undefined, options).format(this.reference)\n\t\t\t} catch {\n\t\t\t\treturn new Intl.DateTimeFormat(undefined, options).format(this.reference)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style of the date line: the heading's colour, or the muted\n\t\t * text colour on a plain header.\n\t\t *\n\t\t * @return {object}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.isPlain && !(this.content && this.content.textColor) ? 'var(--color-text-maxcontrast)' : this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** Whether a non-empty subtitle is set. */\n\t\thasSubtitle() {\n\t\t\treturn this.subtitle !== ''\n\t\t},\n\n\t\t/** Resolved background image URL (file id wins over URL). */\n\t\tbackgroundImageUrl() {\n\t\t\tconst fileId = this.content && this.content.backgroundImageFileId\n\t\t\tif (typeof fileId === 'number' && Number.isFinite(fileId) && fileId > 0) {\n\t\t\t\treturn `/index.php/core/preview?fileId=${fileId}&x=1024&y=512&a=1`\n\t\t\t}\n\t\t\tconst url = this.content && this.content.backgroundImageUrl\n\t\t\t// Accept http(s) plus uploaded/same-origin sources: data: and blob:\n\t\t\t// (from the form's upload) and root-relative paths (NC files/preview).\n\t\t\t// External http(s) images may still be blocked by the instance CSP —\n\t\t\t// uploading is the reliable, same-origin path. resolveImageUrl() adds\n\t\t\t// the webroot + /index.php to `/apps/...` resource paths so they load\n\t\t\t// on index.php-routed instances (other shapes pass through).\n\t\t\tif (typeof url === 'string' && /^(https?:\\/\\/|data:|blob:|\\/)/i.test(url)) {\n\t\t\t\treturn resolveImageUrl(url)\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/** Whether a usable, non-errored background image is present. */\n\t\thasBackgroundImage() {\n\t\t\treturn this.backgroundImageUrl !== '' && this.imageFailed === false\n\t\t},\n\n\t\t/**\n\t\t * Resolved background colour (default theme primary).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tbackgroundColor() {\n\t\t\tconst value = this.content && this.content.backgroundColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.isPlain) {\n\t\t\t\treturn 'transparent'\n\t\t\t}\n\t\t\treturn 'var(--color-primary, #0070c0)'\n\t\t},\n\n\t\t/** Resolved overlay mode (`tint` when an image is present, else `none`). */\n\t\toverlayMode() {\n\t\t\tconst declared = this.content && this.content.overlayMode\n\t\t\tif (typeof declared === 'string' && ALLOWED_OVERLAY_MODES.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.hasBackgroundImage ? 'tint' : 'none'\n\t\t},\n\n\t\t/** Whether an overlay should render. */\n\t\thasOverlay() {\n\t\t\treturn this.overlayMode !== 'none'\n\t\t},\n\n\t\t/** Resolved overlay colour. */\n\t\toverlayColor() {\n\t\t\tconst value = this.content && this.content.overlayColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst bg = this.content && this.content.backgroundColor\n\t\t\treturn (typeof bg === 'string' && bg !== '') ? bg : '#000000'\n\t\t},\n\n\t\t/** Resolved overlay opacity, clamped to 0..1 (default 0.4). */\n\t\toverlayOpacity() {\n\t\t\tconst raw = this.content && this.content.overlayOpacity\n\t\t\tconst value = typeof raw === 'number' ? raw : Number.parseFloat(raw)\n\t\t\tif (Number.isFinite(value) === false) {\n\t\t\t\treturn 0.4\n\t\t\t}\n\t\t\tif (value < 0) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tif (value > 1) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/** Resolved height preset key (default `medium`). */\n\t\theight() {\n\t\t\tconst declared = this.content && this.content.height\n\t\t\tif (typeof declared === 'string' && ALLOWED_HEIGHTS.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'medium'\n\t\t},\n\n\t\t/** The pixel height for the active preset. */\n\t\theightPixels() {\n\t\t\treturn HEIGHT_PIXELS[this.height]\n\t\t},\n\n\t\t/**\n\t\t * Resolved text alignment (default `center`).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextAlign() {\n\t\t\tconst declared = this.content && this.content.textAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_TEXT_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.isPlain ? 'left' : 'center'\n\t\t},\n\n\t\t/** Resolved vertical alignment (default `middle`). */\n\t\tverticalAlign() {\n\t\t\tconst declared = this.content && this.content.verticalAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_VERTICAL_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'middle'\n\t\t},\n\n\t\t/**\n\t\t * Resolved text colour with an auto-contrast fallback.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextColor() {\n\t\t\tconst value = this.content && this.content.textColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\treturn '#ffffff'\n\t\t\t}\n\t\t\tif (this.isPlain && !(this.content && this.content.backgroundColor)) {\n\t\t\t\treturn 'var(--color-main-text)'\n\t\t\t}\n\t\t\treturn this.isLightColor(this.backgroundColor) ? '#000000' : '#ffffff'\n\t\t},\n\n\t\t/** Inline style for the banner wrapper. */\n\t\twrapperStyle() {\n\t\t\tconst style = {\n\t\t\t\tposition: 'relative',\n\t\t\t\twidth: '100%',\n\t\t\t\t// Fill the dashboard grid cell so the banner resizes with the\n\t\t\t\t// widget. A fixed pixel height ignored the cell, which made the\n\t\t\t\t// banner un-resizable and made it overflow/scroll inside a\n\t\t\t\t// smaller cell. The grid controls the size now.\n\t\t\t\theight: '100%',\n\t\t\t\toverflow: 'hidden',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\tstyle['background-image'] = `url(\"${this.backgroundImageUrl}\")`\n\t\t\t\tstyle['background-size'] = 'cover'\n\t\t\t\tstyle['background-position'] = 'center center'\n\t\t\t\tstyle['background-repeat'] = 'no-repeat'\n\t\t\t}\n\t\t\treturn style\n\t\t},\n\n\t\t/** Inline style for the overlay layer. */\n\t\toverlayStyle() {\n\t\t\tif (this.overlayMode === 'gradient-bottom') {\n\t\t\t\treturn {\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\tinset: 0,\n\t\t\t\t\t'background-image': `linear-gradient(to bottom, transparent 50%, ${this.overlayColor} 100%)`,\n\t\t\t\t\t'pointer-events': 'none',\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tposition: 'absolute',\n\t\t\t\tinset: 0,\n\t\t\t\t'background-color': this.overlayColor,\n\t\t\t\topacity: String(this.overlayOpacity),\n\t\t\t\t'pointer-events': 'none',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for the content flex container.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tcontentStyle() {\n\t\t\treturn {\n\t\t\t\tposition: 'relative',\n\t\t\t\t'z-index': 1,\n\t\t\t\t// Beside a view switch the content shares the row instead of\n\t\t\t\t// taking the whole width (see .cn-header-widget--with-views).\n\t\t\t\twidth: this.viewOptions.length > 0 ? 'auto' : '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'flex-direction': 'column',\n\t\t\t\t'align-items': this.flexAlignFromTextAlign,\n\t\t\t\t'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],\n\t\t\t\t// Plain drops the coloured background, not the card padding:\n\t\t\t\t// at 0 the greeting sat flush against the card's left edge.\n\t\t\t\t// Ground has no card, so it has no padding either.\n\t\t\t\tpadding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),\n\t\t\t\t'box-sizing': 'border-box',\n\t\t\t\tgap: this.isGround ? '6px' : '8px',\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t}\n\t\t},\n\n\t\t/** Flex cross-axis alignment derived from the text alignment. */\n\t\tflexAlignFromTextAlign() {\n\t\t\tif (this.textAlign === 'left') {\n\t\t\t\treturn 'flex-start'\n\t\t\t}\n\t\t\tif (this.textAlign === 'right') {\n\t\t\t\treturn 'flex-end'\n\t\t\t}\n\t\t\treturn 'center'\n\t\t},\n\n\t\t/** Inline style applying the resolved text colour. */\n\t\ttextStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** The validated CTA object, or null when incomplete. */\n\t\tcta() {\n\t\t\tconst raw = this.content && this.content.cta\n\t\t\tif (raw === null || typeof raw !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = typeof raw.label === 'string' ? raw.label.trim() : ''\n\t\t\tconst url = typeof raw.url === 'string' ? raw.url.trim() : ''\n\t\t\tif (label === '' || url === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet style = raw.style\n\t\t\tif (typeof style !== 'string' || ALLOWED_CTA_STYLES.includes(style) === false) {\n\t\t\t\tstyle = 'primary'\n\t\t\t}\n\t\t\treturn { label, url, style }\n\t\t},\n\n\t\t/** Whether a complete CTA is present. */\n\t\thasCta() {\n\t\t\treturn this.cta !== null\n\t\t},\n\n\t\t/** The CTA label, translated. */\n\t\tctaLabel() {\n\t\t\treturn this.hasCta && this.cta.label ? this.effectiveTranslate(this.cta.label) : ''\n\t\t},\n\n\t\t/** The CTA URL. */\n\t\tctaUrl() {\n\t\t\treturn this.hasCta ? this.cta.url : ''\n\t\t},\n\n\t\t/** Whether the CTA URL is an external http(s) link. */\n\t\tctaIsExternal() {\n\t\t\treturn this.hasCta && /^https?:\\/\\//i.test(this.ctaUrl)\n\t\t},\n\n\t\t/** The CTA anchor target (`_blank` for external links). */\n\t\tctaTarget() {\n\t\t\treturn this.ctaIsExternal ? '_blank' : null\n\t\t},\n\n\t\t/** The CTA anchor rel for external links. */\n\t\tctaRel() {\n\t\t\treturn this.ctaIsExternal ? 'noopener noreferrer' : null\n\t\t},\n\n\t\t/** The accessible CTA label, annotating external links. */\n\t\tctaAriaLabel() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.ctaIsExternal) {\n\t\t\t\treturn `${this.ctaLabel} (${t('nextcloud-vue', 'opens in new tab')})`\n\t\t\t}\n\t\t\treturn this.ctaLabel\n\t\t},\n\n\t\t/** The CTA class list (base + style modifier). */\n\t\tctaClasses() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t'cn-header-widget__cta',\n\t\t\t\t`cn-header-widget__cta--${this.cta.style}`,\n\t\t\t]\n\t\t},\n\t},\n\n\twatch: {\n\t\tbackgroundImageUrl() {\n\t\t\t// Re-arm the probe so a previously failing URL can recover when\n\t\t\t// the user edits the placement.\n\t\t\tthis.imageFailed = false\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Follow the chosen view: push its route (with the option's `params`\n\t\t * when declared). Nothing happens for the route already shown.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @param {string} route The chosen option's route name.\n\t\t * @return {void}\n\t\t */\n\t\tonViewChange(route) {\n\t\t\tif (!this.$router || !route || route === (this.$route && this.$route.name)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}\n\t\t\tconst target = { name: route }\n\t\t\tif (declared.params && typeof declared.params === 'object') {\n\t\t\t\ttarget.params = declared.params\n\t\t\t}\n\t\t\tthis.$router.push(target)\n\t\t},\n\n\t\t/**\n\t\t * Mark the background image as broken so the renderer falls back to\n\t\t * the solid colour.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonImageError() {\n\t\t\tthis.imageFailed = true\n\t\t},\n\n\t\t/**\n\t\t * Quick perceived-luminance check on a CSS hex colour. Returns true\n\t\t * for \"light\" (white text would be illegible). Non-hex colours\n\t\t * collapse to false (assume dark) — the safer default.\n\t\t *\n\t\t * @param {string} color the CSS colour value.\n\t\t * @return {boolean} true when the colour is perceived as light.\n\t\t */\n\t\tisLightColor(color) {\n\t\t\tif (typeof color !== 'string') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst match = color.trim().match(/^#([0-9a-f]{6})$/i)\n\t\t\tif (!match) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst hex = match[1]\n\t\t\tconst r = Number.parseInt(hex.slice(0, 2), 16)\n\t\t\tconst g = Number.parseInt(hex.slice(2, 4), 16)\n\t\t\tconst b = Number.parseInt(hex.slice(4, 6), 16)\n\t\t\tconst luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255\n\t\t\treturn luma > 0.6\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-header-widget {\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 120px;\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n.cn-header-widget--plain {\n\tmin-height: 0;\n\tborder-radius: 0;\n}\n\n/* With a view switch the heading and the switch share one row, the switch\n at the right and bottom-aligned with the heading; on a narrow card the\n switch wraps under it. */\n.cn-header-widget--with-views {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tjustify-content: space-between;\n\tgap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));\n}\n\n.cn-header-widget--with-views .cn-header-widget__content {\n\tflex: 1 1 320px;\n\twidth: auto;\n}\n\n.cn-header-widget__views {\n\tposition: relative;\n\tz-index: 1;\n\tflex: 0 0 auto;\n\tpadding: 16px;\n}\n\n/* On the page ground (`content.ground`): no card, so no inset around the\n view switch, and the heading at the board's 32px. */\n.cn-header-widget--ground .cn-header-widget__views {\n\tpadding: 0;\n}\n\n.cn-header-widget--ground .cn-header-widget__title {\n\tfont-size: var(--cn-header-ground-title-size, 32px);\n\tletter-spacing: -0.01em;\n}\n\n.cn-header-widget__date {\n\tfont-size: 14px;\n\tline-height: 1.4;\n\tmargin: 0;\n}\n\n.cn-header-widget__overlay {\n\tposition: absolute;\n\tinset: 0;\n}\n\n.cn-header-widget__content {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-header-widget__title {\n\tfont-size: 28px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tmargin: 0;\n}\n\n.cn-header-widget__subtitle {\n\tfont-size: 16px;\n\tline-height: 1.4;\n\tmargin: 0;\n\topacity: 0.95;\n}\n\n.cn-header-widget__cta {\n\tdisplay: inline-block;\n\tmargin-top: 8px;\n\tpadding: 10px 18px;\n\tborder-radius: var(--border-radius, 4px);\n\tfont-size: 14px;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n\ttransition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.cn-header-widget__cta:hover {\n\ttransform: translateY(-1px);\n\tbox-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\n\n.cn-header-widget__cta--primary {\n\tbackground-color: var(--color-primary, #0070c0);\n\tcolor: var(--color-primary-text, #ffffff);\n\tborder: 1px solid var(--color-primary, #0070c0);\n}\n\n.cn-header-widget__cta--secondary {\n\tbackground-color: var(--color-background-hover, rgba(255, 255, 255, 0.85));\n\tcolor: var(--color-main-text, #000000);\n\tborder: 1px solid var(--color-border, #cccccc);\n}\n\n.cn-header-widget__cta--ghost {\n\tbackground-color: transparent;\n\tcolor: inherit;\n\tborder: 1px solid currentColor;\n}\n\n.cn-header-widget__probe {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\topacity: 0;\n\tpointer-events: none;\n\tinset-inline-start: -9999px;\n\ttop: -9999px;\n}\n\n@media print {\n\t.cn-header-widget {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":["CnSegmentedControl","t"],"mappings":";;;;;AA8DA,MAAM,qBAAsB,GAAE,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAA;AAChE,MAAM,eAAc,GAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAA;AAC7D,MAAM,kBAAmB,GAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAA;AACrD,MAAM,yBAAyB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAA;AACzD,MAAM,kBAAiB,GAAI,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAA;;AAE3D,MAAM,aAAY,GAAI,MAAM,CAAC,MAAM,CAAC;AACnC,CAAA,KAAK,EAAE,GAAG;CACV,MAAM,EAAE,GAAG;AACX,CAAA,KAAK,EAAE,GAAG;CACV,MAAM,EAAE,GAAG;AACZ,CAAC,EAAA;;AAED,MAAM,mBAAkB,GAAI,MAAM,CAAC,MAAM,CAAC;AACzC,CAAA,GAAG,EAAE,YAAY;AACjB,CAAA,MAAM,EAAE,QAAQ;AAChB,CAAA,MAAM,EAAE,UAAU;AACnB,CAAC,EAAA;;AAED;;AAEC;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;;;;AAID,aAAe;AACd,CAAA,IAAI,EAAE,gBAAgB;;AAEtB,CAAA,UAAU,EAAE,sBAAEA,YAAoB;;AAElC,CAAA,MAAM,EAAE;AACP;AACC;;;;AAIA;;AAED,EAAA,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,KAAK,KAAK;EAC5C;;AAED,CAAA,KAAK,EAAE;AACN;;AAEC;;AAEA;;AAEA;;;;;;AAMA;AACA;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO,EAAE,CAAC;GACnB;;AAED;AACA;;AAEC;;;EAGD,SAAS,EAAE;GACV,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;AACD;;AAEA;;;AAGC;AACA;;EAED,GAAG,EAAE;AACJ,GAAA,IAAI,EAAE,IAAI;GACV,OAAO,EAAE,IAAI;GACb;EACD;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;;;;AAIN;AACA,GAAA,WAAW,EAAE,KAAK;AACnB,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;;AAEC;AACA;;;AAGD,EAAA,kBAAkB,GAAG;AACpB,GAAA,OAAO,OAAO,IAAI,CAAC,WAAY,KAAI,UAAW,GAAE,IAAI,CAAC,cAAc,CAAC,GAAG,KAAK,GAAE;GAC9E;;AAED;AACA,EAAA,KAAK,GAAG;AACP,GAAA,MAAM,KAAI,GAAI,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,MAAI;AAC/C,GAAA,OAAO,OAAO,KAAI,KAAM,QAAO,IAAK,KAAM,KAAI,EAAC,GAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAA,GAAI,EAAC;GACrF;;;EAGD,QAAQ,GAAG;AACV,GAAA,MAAM,KAAI,GAAI,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,CAAC,SAAO;AAClD,GAAA,OAAO,OAAO,KAAI,KAAM,QAAO,IAAK,KAAM,KAAI,EAAC,GAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAA,GAAI,EAAC;GACrF;;;EAGD,QAAQ,GAAG;GACV,OAAO,IAAI,CAAC,WAAY,KAAI,EAAC;GAC7B;;AAED;;AAEC;;;;EAID,SAAS,GAAG;GACX,OAAO,IAAI,CAAC,GAAE,YAAa,IAAK,GAAE,IAAI,CAAC,GAAI,GAAE,IAAI,IAAI,EAAC;GACtD;;AAED;;;;;AAKC;AACA;;AAEA;;EAED,WAAW,GAAG;AACb,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,KAAM,IAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAM;AAC3E,GAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE;AACzC,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,OAAO,GAAE;KACP,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA,IAAK,OAAO,CAAC,CAAC,KAAI,KAAM,QAAS,IAAG,CAAC,CAAC,KAAM,KAAI,MAAM,OAAO,CAAC,CAAC,UAAU,QAAO,IAAK,CAAC,CAAC,KAAI,KAAM,EAAE,CAAA;KACjH,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAE,EAAC,CAAC,CAAA;GACnE;;AAED;;;AAGC;;AAEA;;EAED,UAAU,GAAG;GACZ,MAAM,OAAM,GAAI,IAAI,CAAC,YAAU;GAC/B,IAAI,OAAO,CAAC,MAAK,KAAM,CAAC,EAAE;AACzB,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,MAAM,UAAU,IAAI,CAAC,MAAO,IAAG,IAAI,CAAC,MAAM,CAAC,KAAG;GAC9C,MAAM,KAAM,GAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,OAAO,EAAA;AACrD,GAAA,OAAO,KAAI,GAAI,KAAK,CAAC,KAAM,GAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAI;GAC5C;;AAED;;AAEC;AACA;;AAEA;;EAED,UAAU,GAAG;AACZ,GAAA,MAAM,KAAI,GAAI,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,UAAQ;GAC/E,OAAO,KAAM,GAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAA,GAAIC,SAAC,CAAC,eAAe,EAAE,MAAM,CAAA;GAClE;;AAED;;AAEC;AACA;;AAED,EAAA,OAAO,GAAG;AACT,GAAA,OAAO,OAAO,CAAC,IAAI,CAAC,OAAQ,KAAI,IAAI,CAAC,OAAO,CAAC,KAAM,KAAI,QAAQ,IAAI,CAAC,OAAO,CAAC,MAAK,KAAM,IAAI,CAAC,CAAA;GAC5F;;AAED;;;;AAIC;AACA;AACA;AACA;;EAED,QAAQ,GAAG;GACV,OAAO,OAAO,CAAC,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,MAAO,KAAI,IAAI,CAAA;GAC3D;;AAED;;;AAGC;AACA;;;AAGD,EAAA,YAAY,GAAG;AACd,GAAA,IAAI,KAAG;AACP,GAAA,IAAI;AACH,IAAA,IAAG,GAAI,cAAc,GAAC;AACvB,IAAA,CAAE,MAAM;AACP,IAAA,IAAG,GAAI,KAAG;AACX,IAAA;AACA,GAAA,MAAM,WAAY,GAAE,CAAC,IAAG,IAAK,OAAO,IAAI,CAAC,WAAY,KAAI,QAAQ,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAC,GAAI,GAAC;AAChG,GAAA,IAAI,WAAY,KAAI,EAAE,EAAE;AACvB,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,OAAO,CAAC,QAAO,KAAM,MAAO,GAAE,WAAU,GAAI,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;GAClF;;AAED;;;AAGC;AACA;;;AAGD,EAAA,YAAY,GAAG;GACd,MAAM,QAAS,GAAE,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,SAAO;GACrD,IAAI,QAAS,KAAI,IAAK,IAAG,QAAS,KAAI,MAAO,IAAG,QAAO,KAAM,OAAO,EAAE;AACrE,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,MAAM,IAAK,GAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,GAAC;GACrC,MAAM,IAAG,GAAI,IAAI,CAAC,aAAW;GAC7B,IAAI,IAAK,GAAE,EAAE,EAAE;AACd,IAAA,OAAO,IAAG,GAAIA,SAAC,CAAC,eAAe,EAAE,sBAAsB,EAAE,EAAE,IAAG,EAAG,CAAE,GAAEA,SAAC,CAAC,eAAe,EAAE,cAAc,CAAA;AACvG,IAAA;GACA,IAAI,IAAK,GAAE,EAAE,EAAE;AACd,IAAA,OAAO,IAAG,GAAIA,SAAC,CAAC,eAAe,EAAE,wBAAwB,EAAE,EAAE,IAAK,EAAC,IAAIA,SAAC,CAAC,eAAe,EAAE,gBAAgB,CAAA;AAC3G,IAAA;AACA,GAAA,OAAO,IAAG,GAAIA,SAAC,CAAC,eAAe,EAAE,sBAAsB,EAAE,EAAE,IAAG,EAAG,CAAE,GAAEA,SAAC,CAAC,eAAe,EAAE,cAAc,CAAA;GACtG;;AAED;;AAEC;AACA;;;EAGD,WAAW,GAAG;AACb,GAAA,OAAO,IAAI,CAAC,YAAW,IAAK,IAAI,CAAC,KAAI;GACrC;;AAED;;AAEC;AACA;AACA;;;EAGD,QAAQ,GAAG;GACV,IAAI,CAAC,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE;AACpD,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,MAAM,OAAM,GAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAU,GAAA;AAClF,GAAA,IAAI,OAAK;AACT,GAAA,IAAI;IACH,MAAK,GAAI,kBAAkB,GAAC;AAC7B,IAAA,CAAE,MAAM;AACP,IAAA,MAAO,GAAE,UAAQ;AAClB,IAAA;AACA,GAAA,IAAI;AACH,IAAA,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,MAAK,IAAK,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAA;AACnF,IAAA,CAAE,MAAM;AACP,IAAA,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAA;AACzE,IAAA;GACA;;AAED;;;AAGC;AACA;;;EAGD,SAAS,GAAG;AACX,GAAA,OAAO;IACN,KAAK,EAAE,IAAI,CAAC,OAAQ,IAAG,EAAE,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAA,GAAI,+BAA8B,GAAI,IAAI,CAAC,SAAS;AACnH,IAAA,MAAM,EAAE,CAAC;AACV,IAAA;GACA;;AAED;EACA,WAAW,GAAG;GACb,OAAO,IAAI,CAAC,QAAS,KAAI,EAAC;GAC1B;;;AAGD,EAAA,kBAAkB,GAAG;GACpB,MAAM,MAAO,GAAE,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,sBAAoB;AAChE,GAAA,IAAI,OAAO,MAAK,KAAM,QAAS,IAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAA,IAAK,MAAK,GAAI,CAAC,EAAE;AACxE,IAAA,OAAO,CAAC,+BAA+B,EAAE,MAAM,CAAC,iBAAiB,CAAA;AAClE,IAAA;GACA,MAAM,GAAE,GAAI,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,mBAAiB;;;;;;;AAO1D,GAAA,IAAI,OAAO,GAAI,KAAI,YAAY,gCAAgC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;IAC1E,OAAO,eAAe,CAAC,GAAG,CAAA;AAC3B,IAAA;AACA,GAAA,OAAO,EAAC;GACR;;;AAGD,EAAA,kBAAkB,GAAG;GACpB,OAAO,IAAI,CAAC,kBAAiB,KAAM,EAAC,IAAK,IAAI,CAAC,WAAU,KAAM,KAAI;GAClE;;AAED;;AAEC;;;AAGD,EAAA,eAAe,GAAG;GACjB,MAAM,KAAI,GAAI,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,gBAAc;AACzD,GAAA,IAAI,OAAO,KAAI,KAAM,QAAS,IAAG,UAAU,EAAE,EAAE;IAC9C,OAAO,KAAI;AACZ,IAAA;AACA,GAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AACjB,IAAA,OAAO,aAAY;AACpB,IAAA;AACA,GAAA,OAAO,+BAA8B;GACrC;;AAED;EACA,WAAW,GAAG;GACb,MAAM,WAAW,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,CAAC,YAAU;AACxD,GAAA,IAAI,OAAO,QAAO,KAAM,QAAO,IAAK,qBAAqB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;IAC7E,OAAO,QAAO;AACf,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,kBAAiB,GAAI,MAAO,GAAE,MAAK;GAC/C;;AAED;EACA,UAAU,GAAG;AACZ,GAAA,OAAO,IAAI,CAAC,WAAY,KAAI,MAAK;GACjC;;;AAGD,EAAA,YAAY,GAAG;GACd,MAAM,KAAM,GAAE,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,aAAW;AACtD,GAAA,IAAI,OAAO,KAAI,KAAM,QAAS,IAAG,UAAU,EAAE,EAAE;IAC9C,OAAO,KAAI;AACZ,IAAA;GACA,MAAM,EAAG,GAAE,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,gBAAc;GACtD,OAAO,CAAC,OAAO,OAAO,QAAS,IAAG,EAAC,KAAM,EAAE,IAAI,EAAC,GAAI,SAAQ;GAC5D;;;AAGD,EAAA,cAAc,GAAG;GAChB,MAAM,GAAE,GAAI,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,eAAa;GACtD,MAAM,KAAM,GAAE,OAAO,GAAE,KAAM,QAAO,GAAI,GAAE,GAAI,MAAM,CAAC,UAAU,CAAC,GAAG,EAAA;AACnE,GAAA,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAE,KAAI,KAAK,EAAE;AACrC,IAAA,OAAO,GAAE;AACV,IAAA;GACA,IAAI,KAAI,GAAI,CAAC,EAAE;AACd,IAAA,OAAO,CAAA;AACR,IAAA;GACA,IAAI,KAAI,GAAI,CAAC,EAAE;AACd,IAAA,OAAO,CAAA;AACR,IAAA;GACA,OAAO,KAAI;GACX;;AAED;AACA,EAAA,MAAM,GAAG;GACR,MAAM,QAAS,GAAE,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,OAAK;AACnD,GAAA,IAAI,OAAO,QAAO,KAAM,QAAS,IAAG,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;IACvE,OAAO,QAAO;AACf,IAAA;GACA,OAAO,QAAO;GACd;;AAED;AACA,EAAA,YAAY,GAAG;AACd,GAAA,OAAO,aAAa,CAAC,IAAI,CAAC,MAAM,CAAA;GAChC;;AAED;AACC;AACA;;;EAGD,SAAS,GAAG;GACX,MAAM,QAAS,GAAE,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,OAAO,CAAC,UAAQ;AACtD,GAAA,IAAI,OAAO,QAAS,KAAI,QAAS,IAAG,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;IAC1E,OAAO,QAAO;AACf,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,OAAM,GAAI,MAAK,GAAI,QAAO;GACtC;;;AAGD,EAAA,aAAa,GAAG;GACf,MAAM,WAAW,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,cAAY;AAC1D,GAAA,IAAI,OAAO,QAAO,KAAM,QAAO,IAAK,sBAAsB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;IAC9E,OAAO,QAAO;AACf,IAAA;GACA,OAAO,QAAO;GACd;;AAED;;AAEC;;;EAGD,SAAS,GAAG;GACX,MAAM,KAAM,GAAE,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,UAAQ;AACnD,GAAA,IAAI,OAAO,KAAI,KAAM,QAAS,IAAG,UAAU,EAAE,EAAE;IAC9C,OAAO,KAAI;AACZ,IAAA;GACA,IAAI,IAAI,CAAC,kBAAkB,EAAE;AAC5B,IAAA,OAAO,SAAQ;AAChB,IAAA;AACA,GAAA,IAAI,IAAI,CAAC,OAAM,IAAK,EAAE,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE;IACpE,OAAO,wBAAuB;AAC/B,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,eAAe,CAAE,GAAE,SAAU,GAAE,SAAQ;GACrE;;AAED;AACA,EAAA,YAAY,GAAG;GACd,MAAM,KAAI,GAAI;AACb,IAAA,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,MAAM;;;;AAIb;IACA,MAAM,EAAE,MAAM;AACd,IAAA,QAAQ,EAAE,QAAQ;AAClB,IAAA,kBAAkB,EAAE,IAAI,CAAC,eAAe;AACzC,KAAA;GACA,IAAI,IAAI,CAAC,kBAAkB,EAAE;IAC5B,KAAK,CAAC,kBAAkB,CAAE,GAAE,CAAC,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC,EAAE,EAAA;AAC9D,IAAA,KAAK,CAAC,iBAAiB,CAAE,GAAE,QAAM;AACjC,IAAA,KAAK,CAAC,qBAAqB,CAAE,GAAE,gBAAc;AAC7C,IAAA,KAAK,CAAC,mBAAmB,IAAI,YAAU;AACxC,IAAA;GACA,OAAO,KAAI;GACX;;AAED;AACA,EAAA,YAAY,GAAG;AACd,GAAA,IAAI,IAAI,CAAC,WAAY,KAAI,iBAAiB,EAAE;AAC3C,IAAA,OAAO;AACN,KAAA,QAAQ,EAAE,UAAU;AACpB,KAAA,KAAK,EAAE,CAAC;AACR,KAAA,kBAAkB,EAAE,CAAC,4CAA4C,EAAE,IAAI,CAAC,YAAY,CAAE,MAAK,CAAC;KAC5F,gBAAgB,EAAE,MAAM;AACzB,KAAA;AACD,IAAA;AACA,GAAA,OAAO;AACN,IAAA,QAAQ,EAAE,UAAU;AACpB,IAAA,KAAK,EAAE,CAAC;AACR,IAAA,kBAAkB,EAAE,IAAI,CAAC,YAAY;AACrC,IAAA,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC;IACpC,gBAAgB,EAAE,MAAM;AACzB,IAAA;GACA;;AAED;AACC;AACA;;;AAGD,EAAA,YAAY,GAAG;AACd,GAAA,OAAO;AACN,IAAA,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,CAAC;;;AAGZ,IAAA,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,CAAA,GAAI,SAAS,MAAM;IACpD,MAAM,EAAE,MAAM;AACd,IAAA,OAAO,EAAE,MAAM;IACf,gBAAgB,EAAE,QAAQ;AAC1B,IAAA,aAAa,EAAE,IAAI,CAAC,sBAAsB;IAC1C,iBAAiB,EAAE,mBAAmB,CAAC,IAAI,CAAC,aAAa,CAAC;;;AAG1D;IACA,OAAO,EAAE,IAAI,CAAC,QAAO,GAAI,GAAE,IAAK,IAAI,CAAC,OAAQ,GAAE,MAAO,GAAE,WAAW,CAAC;IACpE,YAAY,EAAE,YAAY;IAC1B,GAAG,EAAE,IAAI,CAAC,QAAS,GAAE,KAAM,GAAE,KAAK;IAClC,YAAY,EAAE,IAAI,CAAC,SAAS;AAC7B,IAAA;GACA;;;EAGD,sBAAsB,GAAG;GACxB,IAAI,IAAI,CAAC,SAAU,KAAI,MAAM,EAAE;AAC9B,IAAA,OAAO,YAAW;AACnB,IAAA;GACA,IAAI,IAAI,CAAC,SAAQ,KAAM,OAAO,EAAE;AAC/B,IAAA,OAAO,UAAS;AACjB,IAAA;GACA,OAAO,QAAO;GACd;;AAED;EACA,SAAS,GAAG;AACX,GAAA,OAAO;AACN,IAAA,KAAK,EAAE,IAAI,CAAC,SAAS;AACrB,IAAA,MAAM,EAAE,CAAC;AACV,IAAA;GACA;;;EAGD,GAAG,GAAG;AACL,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,OAAM,IAAK,IAAI,CAAC,OAAO,CAAC,IAAE;AAC3C,GAAA,IAAI,QAAQ,IAAG,IAAK,OAAO,GAAE,KAAM,QAAQ,EAAE;AAC5C,IAAA,OAAO,IAAG;AACX,IAAA;GACA,MAAM,KAAM,GAAE,OAAO,GAAG,CAAC,KAAI,KAAM,QAAS,GAAE,GAAG,CAAC,KAAK,CAAC,IAAI,EAAG,GAAE,GAAC;GAClE,MAAM,GAAE,GAAI,OAAO,GAAG,CAAC,GAAI,KAAI,QAAS,GAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAG,GAAE,GAAC;GAC5D,IAAI,KAAI,KAAM,MAAM,GAAE,KAAM,EAAE,EAAE;AAC/B,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,IAAI,KAAI,GAAI,GAAG,CAAC,MAAI;AACpB,GAAA,IAAI,OAAO,KAAI,KAAM,QAAS,IAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAE,KAAI,KAAK,EAAE;AAC9E,IAAA,KAAI,GAAI,UAAQ;AACjB,IAAA;GACA,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,KAAM,EAAA;GAC3B;;AAED;AACA,EAAA,MAAM,GAAG;AACR,GAAA,OAAO,IAAI,CAAC,GAAI,KAAI,IAAG;GACvB;;;EAGD,QAAQ,GAAG;AACV,GAAA,OAAO,IAAI,CAAC,MAAO,IAAG,IAAI,CAAC,GAAG,CAAC,KAAM,GAAE,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAA,GAAI,EAAC;GAClF;;AAED;AACA,EAAA,MAAM,GAAG;AACR,GAAA,OAAO,IAAI,CAAC,MAAO,GAAE,IAAI,CAAC,GAAG,CAAC,GAAE,GAAI,EAAC;GACrC;;AAED;AACA,EAAA,aAAa,GAAG;GACf,OAAO,IAAI,CAAC,MAAO,IAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAA;GACtD;;;EAGD,SAAS,GAAG;AACX,GAAA,OAAO,IAAI,CAAC,aAAc,GAAE,QAAO,GAAI,IAAG;GAC1C;;AAED;AACA,EAAA,MAAM,GAAG;GACR,OAAO,IAAI,CAAC,aAAY,GAAI,qBAAsB,GAAE,IAAG;GACvD;;;AAGD,EAAA,YAAY,GAAG;GACd,IAAI,IAAI,CAAC,MAAK,KAAM,KAAK,EAAE;AAC1B,IAAA,OAAO,IAAG;AACX,IAAA;GACA,IAAI,IAAI,CAAC,aAAa,EAAE;AACvB,IAAA,OAAO,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAA,EAAG,EAAEA,SAAC,CAAC,eAAe,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAA;AACrE,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,QAAO;GACnB;;AAED;EACA,UAAU,GAAG;GACZ,IAAI,IAAI,CAAC,MAAK,KAAM,KAAK,EAAE;AAC1B,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,OAAO;IACN,uBAAuB;AACvB,IAAA,CAAC,uBAAuB,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC3C,IAAA;GACA;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,kBAAkB,GAAG;;;AAGpB,GAAA,IAAI,CAAC,WAAU,GAAI,MAAI;GACvB;EACD;;AAED,CAAA,OAAO,EAAE;AACR;;;AAGC;;;;;AAKD,EAAA,YAAY,CAAC,KAAK,EAAE;AACnB,GAAA,IAAI,CAAC,IAAI,CAAC,OAAM,IAAK,CAAC,KAAI,IAAK,WAAW,IAAI,CAAC,MAAO,IAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;IAC3E,MAAK;AACN,IAAA;AACA,GAAA,MAAM,QAAS,GAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAM,IAAK,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAE,IAAG,CAAC,CAAC,UAAU,KAAK,CAAE,IAAG,GAAC;GAC5F,MAAM,MAAK,GAAI,EAAE,IAAI,EAAE,KAAM,GAAA;GAC7B,IAAI,QAAQ,CAAC,MAAO,IAAG,OAAO,QAAQ,CAAC,WAAW,QAAQ,EAAE;IAC3D,MAAM,CAAC,MAAO,GAAE,QAAQ,CAAC,OAAK;AAC/B,IAAA;GACA,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAA;GACxB;;AAED;;AAEC;AACA;;;AAGD,EAAA,YAAY,GAAG;AACd,GAAA,IAAI,CAAC,cAAc,KAAG;GACtB;;AAED;;;AAGC;AACA;AACA;;;AAGD,EAAA,YAAY,CAAC,KAAK,EAAE;GACnB,IAAI,OAAO,UAAU,QAAQ,EAAE;IAC9B,OAAO,KAAI;AACZ,IAAA;GACA,MAAM,KAAM,GAAE,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,mBAAmB,EAAA;GACpD,IAAI,CAAC,KAAK,EAAE;IACX,OAAO,KAAI;AACZ,IAAA;AACA,GAAA,MAAM,GAAE,GAAI,KAAK,CAAC,CAAC,EAAA;AACnB,GAAA,MAAM,CAAA,GAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAA;AAC7C,GAAA,MAAM,CAAA,GAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAA;AAC7C,GAAA,MAAM,CAAA,GAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAA;AAC7C,GAAA,MAAM,IAAG,GAAI,CAAC,KAAI,GAAI,CAAE,GAAE,KAAI,GAAI,CAAA,GAAI,QAAQ,CAAC,IAAI,IAAE;GACrD,OAAO,IAAG,GAAI,GAAE;GAChB;EACD;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"CnHeaderWidget.vue2.js","sources":["../../../../src/components/CnHeaderWidget/CnHeaderWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<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\">\n\t\t<div\n\t\t\tv-if=\"hasOverlay\"\n\t\t\tclass=\"cn-header-widget__overlay\"\n\t\t\t:style=\"overlayStyle\"\n\t\t\taria-hidden=\"true\" />\n\t\t<div class=\"cn-header-widget__content\" :style=\"contentStyle\">\n\t\t\t<p\n\t\t\t\tv-if=\"dateLine\"\n\t\t\t\tclass=\"cn-header-widget__date\"\n\t\t\t\t:style=\"dateStyle\"\n\t\t\t\tdata-testid=\"cn-header-widget-date\">\n\t\t\t\t{{ dateLine }}\n\t\t\t</p>\n\t\t\t<h2 v-if=\"hasTitle\" class=\"cn-header-widget__title\" :style=\"textStyle\">\n\t\t\t\t{{ headingText }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"hasSubtitle\" class=\"cn-header-widget__subtitle\" :style=\"textStyle\">\n\t\t\t\t{{ subtitle }}\n\t\t\t</p>\n\t\t\t<a\n\t\t\t\tv-if=\"hasCta\"\n\t\t\t\t:href=\"ctaUrl\"\n\t\t\t\t:class=\"ctaClasses\"\n\t\t\t\t:target=\"ctaTarget\"\n\t\t\t\t:rel=\"ctaRel\"\n\t\t\t\t:aria-label=\"ctaAriaLabel\">\n\t\t\t\t{{ ctaLabel }}\n\t\t\t</a>\n\t\t</div>\n\t\t<!-- A view switch at the right of the heading (`content.views`): \"My\n\t\t work / My team\" as a segmented control whose options are routes,\n\t\t or views of the page the widget sits on (`option.view`); a view\n\t\t option controls the page's view region. -->\n\t\t<div v-if=\"viewOptions.length > 0\" class=\"cn-header-widget__views\">\n\t\t\t<CnSegmentedControl\n\t\t\t\t:options=\"viewOptions\"\n\t\t\t\t:modelValue=\"activeView\"\n\t\t\t\t:ariaLabel=\"viewsLabel\"\n\t\t\t\t:controls=\"viewsRegionId\"\n\t\t\t\tdata-testid=\"cn-header-widget-views\"\n\t\t\t\t@update:modelValue=\"onViewChange\" />\n\t\t</div>\n\t\t<img\n\t\t\tv-if=\"hasBackgroundImage\"\n\t\t\tclass=\"cn-header-widget__probe\"\n\t\t\t:src=\"backgroundImageUrl\"\n\t\t\talt=\"\"\n\t\t\taria-hidden=\"true\"\n\t\t\t@error=\"onImageError\">\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { getCanonicalLocale, translate as t } from '@nextcloud/l10n'\nimport CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'\nimport { resolveImageUrl } from '../../utils/resolveImageUrl.js'\n\nconst ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']\nconst ALLOWED_HEIGHTS = ['small', 'medium', 'large', 'xlarge']\nconst ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']\nconst ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']\nconst ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']\n\n/** Marks a switch option that selects a page view rather than a route. */\nconst VIEW_OPTION_PREFIX = 'view:'\n\nconst HEIGHT_PIXELS = Object.freeze({\n\tsmall: 120,\n\tmedium: 200,\n\tlarge: 320,\n\txlarge: 480,\n})\n\nconst VERTICAL_ALIGN_FLEX = Object.freeze({\n\ttop: 'flex-start',\n\tmiddle: 'center',\n\tbottom: 'flex-end',\n})\n\n/**\n * CnHeaderWidget — full-width banner widget for dashboards. Renders a\n * configurable header with title, optional subtitle, optional background\n * image, optional colour overlay, and optional call-to-action button.\n *\n * Image source precedence: `backgroundImageFileId` takes priority over\n * `backgroundImageUrl` (resolved via the Nextcloud core preview route). When\n * the image fails to load the renderer silently falls back to the solid\n * `backgroundColor`; no error UI is shown.\n *\n * Height presets map to fixed pixels: small=120, medium=200, large=320,\n * xlarge=480. Title is `<h2>`, subtitle `<p>`, CTA `<a>` — all semantic HTML.\n * External CTA URLs (http/https) open in a new tab with\n * `rel=\"noopener noreferrer\"`; relative / anchor URLs stay in the current tab.\n *\n * Optional greeting: `greeting: true` makes the heading greet the signed-in\n * user by time of day and first name (\"Good afternoon, Pieter\"; `\"full\"` uses\n * the whole display name), and `showDate: true` adds a line with today's\n * date above it. `plain: true` drops the coloured background and aligns the\n * text to the start, which is how a greeting usually sits on a dashboard.\n *\n * ```json\n * { \"widgetKey\": \"header\", \"props\": { \"content\": { \"greeting\": true, \"showDate\": true, \"plain\": true } } }\n * ```\n *\n * Registered as the `header` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnHeaderWidget',\n\n\tcomponents: { CnSegmentedControl },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.title` / `content.subtitle` / CTA label\n\t\t * are run through it. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * The views of the page this widget sits on, provided by\n\t\t * CnDashboardPage / CnDetailPage (`views`). An option with a `view`\n\t\t * selects one of them. Null outside such a page.\n\t\t */\n\t\tcnPageViews: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content: `{title, subtitle, backgroundImageUrl,\n\t\t * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,\n\t\t * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,\n\t\t * greeting, showDate, plain, ground, views}`. All fields are optional except\n\t\t * `title` (or `greeting`); unknown enum values collapse to documented\n\t\t * defaults and the renderer never throws. `views` is\n\t\t * `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:\n\t\t * a segmented control at the right of the heading whose checked option\n\t\t * is the current route; choosing another pushes its route. An option\n\t\t * with `view` selects that view of the page the widget sits on (the\n\t\t * page's `views`) instead, and the page then draws no switch of its\n\t\t * own. `ground: true`\n\t\t * draws the greeting on the page ground: the plain look with no\n\t\t * padding, a 32px heading and the date line 6px above it (a dashboard\n\t\t * also drops the widget's card for it).\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/* eslint-disable vue/no-unused-properties -- part of the widget contract: CnContainerChild and the dashboard pages bind :placement on every widget, so declaring it keeps it out of $attrs */\n\t\t/**\n\t\t * Placement entity — reserved to match the renderer contract.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/**\n\t\t * The moment the greeting and the date line are computed for. Leave\n\t\t * empty for the current time.\n\t\t *\n\t\t * @type {Date|null}\n\t\t */\n\t\tnow: {\n\t\t\ttype: Date,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t// Set true once the background image fires a DOM `error` so\n\t\t\t// subsequent renders fall back to `backgroundColor` only. The\n\t\t\t// probe `<img>` is invisible — its only job is load-failure\n\t\t\t// detection for the CSS background.\n\t\t\timageFailed: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the injected `cnTranslate` (the host\n\t\t * app's bound `t()`), identity by default.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t},\n\n\t\t/** The header title, translated, or '' when absent. */\n\t\ttitle() {\n\t\t\tconst value = this.content && this.content.title\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** The header subtitle, translated, or '' when absent. */\n\t\tsubtitle() {\n\t\t\tconst value = this.content && this.content.subtitle\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** Whether a heading renders: a non-empty title or a greeting. */\n\t\thasTitle() {\n\t\t\treturn this.headingText !== ''\n\t\t},\n\n\t\t/**\n\t\t * The moment the greeting and date line are computed for.\n\t\t *\n\t\t * @return {Date}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\treference() {\n\t\t\treturn this.now instanceof Date ? this.now : new Date()\n\t\t},\n\n\t\t/**\n\t\t * The view switch's options from `content.views.options`: each\n\t\t * `{ label, route, params? }` with a non-empty label and route becomes\n\t\t * a segmented-control option keyed on its route name (the label goes\n\t\t * through the host translate function). Empty without a router, so\n\t\t * the switch cannot offer routes it cannot follow.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewOptions() {\n\t\t\tconst raw = this.content && this.content.views && this.content.views.options\n\t\t\tif (!Array.isArray(raw)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst pageViewIds = this.pageViewIds\n\t\t\treturn raw\n\t\t\t\t.filter((o) => o && typeof o.label === 'string' && o.label !== '')\n\t\t\t\t.map((o) => {\n\t\t\t\t\tif (typeof o.view === 'string' && o.view !== '') {\n\t\t\t\t\t\treturn pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof o.route === 'string' && o.route !== '' && this.$router) {\n\t\t\t\t\t\treturn { value: o.route, label: this.cnTranslate(o.label) }\n\t\t\t\t\t}\n\t\t\t\t\treturn null\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * The ids of the page's views, empty outside a page with views.\n\t\t *\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 * @return {string[]}\n\t\t */\n\t\tpageViewIds() {\n\t\t\tconst views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : []\n\t\t\treturn views.map((view) => view.id)\n\t\t},\n\n\t\t/**\n\t\t * The id of the page's view region when an option selects a view,\n\t\t * for the options' `aria-controls`; '' otherwise.\n\t\t *\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t * @return {string}\n\t\t */\n\t\tviewsRegionId() {\n\t\t\tconst hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX))\n\t\t\treturn hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''\n\t\t},\n\n\t\t/**\n\t\t * The checked view: the option whose route is the current route, else\n\t\t * the first option (a switch always has one checked option).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string|null}\n\t\t */\n\t\tactiveView() {\n\t\t\tconst options = this.viewOptions\n\t\t\tif (options.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst activeView = this.cnPageViews ? this.cnPageViews.activeId : null\n\t\t\tconst viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null\n\t\t\tif (viewMatch) {\n\t\t\t\treturn viewMatch.value\n\t\t\t}\n\t\t\tconst current = this.$route && this.$route.name\n\t\t\tconst match = options.find((o) => o.value === current)\n\t\t\treturn match ? match.value : options[0].value\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch: `content.views.ariaLabel`\n\t\t * through the host translate function, else \"View\".\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string}\n\t\t */\n\t\tviewsLabel() {\n\t\t\tconst label = this.content && this.content.views && this.content.views.ariaLabel\n\t\t\treturn label ? this.cnTranslate(label) : t('nextcloud-vue', 'Views')\n\t\t},\n\n\t\t/**\n\t\t * Whether the plain presentation is on (no coloured background).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisPlain() {\n\t\t\treturn Boolean(this.content && (this.content.plain === true || this.content.ground === true))\n\t\t},\n\n\t\t/**\n\t\t * Whether the greeting sits on the page ground (`content.ground`):\n\t\t * the plain look without the card padding, so the date line and the\n\t\t * heading align with the page edge as the board draws them. Off by\n\t\t * default.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground\n\t\t * @return {boolean}\n\t\t */\n\t\tisGround() {\n\t\t\treturn Boolean(this.content && this.content.ground === true)\n\t\t},\n\n\t\t/**\n\t\t * The name the greeting uses: the first word of the display name, or\n\t\t * the whole display name for `greeting: \"full\"`. '' when unknown.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingName() {\n\t\t\tlet user\n\t\t\ttry {\n\t\t\t\tuser = getCurrentUser()\n\t\t\t} catch {\n\t\t\t\tuser = null\n\t\t\t}\n\t\t\tconst displayName = (user && typeof user.displayName === 'string') ? user.displayName.trim() : ''\n\t\t\tif (displayName === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.content.greeting === 'full' ? displayName : displayName.split(/\\s+/)[0]\n\t\t},\n\n\t\t/**\n\t\t * The greeting for the time of day, with the user's name when known,\n\t\t * or '' when the greeting is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingText() {\n\t\t\tconst greeting = this.content && this.content.greeting\n\t\t\tif (greeting !== true && greeting !== 'full' && greeting !== 'first') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst hour = this.reference.getHours()\n\t\t\tconst name = this.greetingName\n\t\t\tif (hour < 12) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good morning, {name}', { name }) : t('nextcloud-vue', 'Good morning')\n\t\t\t}\n\t\t\tif (hour < 18) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good afternoon, {name}', { name }) : t('nextcloud-vue', 'Good afternoon')\n\t\t\t}\n\t\t\treturn name ? t('nextcloud-vue', 'Good evening, {name}', { name }) : t('nextcloud-vue', 'Good evening')\n\t\t},\n\n\t\t/**\n\t\t * The heading: the greeting when it is on, else the title.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\theadingText() {\n\t\t\treturn this.greetingText || this.title\n\t\t},\n\n\t\t/**\n\t\t * Today's date written out (\"Monday 5 October 2026\"), or '' when\n\t\t * `showDate` is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateLine() {\n\t\t\tif (!this.content || this.content.showDate !== true) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }\n\t\t\tlet locale\n\t\t\ttry {\n\t\t\t\tlocale = getCanonicalLocale()\n\t\t\t} catch {\n\t\t\t\tlocale = undefined\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale || undefined, options).format(this.reference)\n\t\t\t} catch {\n\t\t\t\treturn new Intl.DateTimeFormat(undefined, options).format(this.reference)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style of the date line: the heading's colour, or the muted\n\t\t * text colour on a plain header.\n\t\t *\n\t\t * @return {object}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.isPlain && !(this.content && this.content.textColor) ? 'var(--color-text-maxcontrast)' : this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** Whether a non-empty subtitle is set. */\n\t\thasSubtitle() {\n\t\t\treturn this.subtitle !== ''\n\t\t},\n\n\t\t/** Resolved background image URL (file id wins over URL). */\n\t\tbackgroundImageUrl() {\n\t\t\tconst fileId = this.content && this.content.backgroundImageFileId\n\t\t\tif (typeof fileId === 'number' && Number.isFinite(fileId) && fileId > 0) {\n\t\t\t\treturn `/index.php/core/preview?fileId=${fileId}&x=1024&y=512&a=1`\n\t\t\t}\n\t\t\tconst url = this.content && this.content.backgroundImageUrl\n\t\t\t// Accept http(s) plus uploaded/same-origin sources: data: and blob:\n\t\t\t// (from the form's upload) and root-relative paths (NC files/preview).\n\t\t\t// External http(s) images may still be blocked by the instance CSP —\n\t\t\t// uploading is the reliable, same-origin path. resolveImageUrl() adds\n\t\t\t// the webroot + /index.php to `/apps/...` resource paths so they load\n\t\t\t// on index.php-routed instances (other shapes pass through).\n\t\t\tif (typeof url === 'string' && /^(https?:\\/\\/|data:|blob:|\\/)/i.test(url)) {\n\t\t\t\treturn resolveImageUrl(url)\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/** Whether a usable, non-errored background image is present. */\n\t\thasBackgroundImage() {\n\t\t\treturn this.backgroundImageUrl !== '' && this.imageFailed === false\n\t\t},\n\n\t\t/**\n\t\t * Resolved background colour (default theme primary).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tbackgroundColor() {\n\t\t\tconst value = this.content && this.content.backgroundColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.isPlain) {\n\t\t\t\treturn 'transparent'\n\t\t\t}\n\t\t\treturn 'var(--color-primary, #0070c0)'\n\t\t},\n\n\t\t/** Resolved overlay mode (`tint` when an image is present, else `none`). */\n\t\toverlayMode() {\n\t\t\tconst declared = this.content && this.content.overlayMode\n\t\t\tif (typeof declared === 'string' && ALLOWED_OVERLAY_MODES.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.hasBackgroundImage ? 'tint' : 'none'\n\t\t},\n\n\t\t/** Whether an overlay should render. */\n\t\thasOverlay() {\n\t\t\treturn this.overlayMode !== 'none'\n\t\t},\n\n\t\t/** Resolved overlay colour. */\n\t\toverlayColor() {\n\t\t\tconst value = this.content && this.content.overlayColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst bg = this.content && this.content.backgroundColor\n\t\t\treturn (typeof bg === 'string' && bg !== '') ? bg : '#000000'\n\t\t},\n\n\t\t/** Resolved overlay opacity, clamped to 0..1 (default 0.4). */\n\t\toverlayOpacity() {\n\t\t\tconst raw = this.content && this.content.overlayOpacity\n\t\t\tconst value = typeof raw === 'number' ? raw : Number.parseFloat(raw)\n\t\t\tif (Number.isFinite(value) === false) {\n\t\t\t\treturn 0.4\n\t\t\t}\n\t\t\tif (value < 0) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tif (value > 1) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/** Resolved height preset key (default `medium`). */\n\t\theight() {\n\t\t\tconst declared = this.content && this.content.height\n\t\t\tif (typeof declared === 'string' && ALLOWED_HEIGHTS.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'medium'\n\t\t},\n\n\t\t/** The pixel height for the active preset. */\n\t\theightPixels() {\n\t\t\treturn HEIGHT_PIXELS[this.height]\n\t\t},\n\n\t\t/**\n\t\t * Resolved text alignment (default `center`).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextAlign() {\n\t\t\tconst declared = this.content && this.content.textAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_TEXT_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.isPlain ? 'left' : 'center'\n\t\t},\n\n\t\t/** Resolved vertical alignment (default `middle`). */\n\t\tverticalAlign() {\n\t\t\tconst declared = this.content && this.content.verticalAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_VERTICAL_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'middle'\n\t\t},\n\n\t\t/**\n\t\t * Resolved text colour with an auto-contrast fallback.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextColor() {\n\t\t\tconst value = this.content && this.content.textColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\treturn '#ffffff'\n\t\t\t}\n\t\t\tif (this.isPlain && !(this.content && this.content.backgroundColor)) {\n\t\t\t\treturn 'var(--color-main-text)'\n\t\t\t}\n\t\t\treturn this.isLightColor(this.backgroundColor) ? '#000000' : '#ffffff'\n\t\t},\n\n\t\t/** Inline style for the banner wrapper. */\n\t\twrapperStyle() {\n\t\t\tconst style = {\n\t\t\t\tposition: 'relative',\n\t\t\t\twidth: '100%',\n\t\t\t\t// Fill the dashboard grid cell so the banner resizes with the\n\t\t\t\t// widget. A fixed pixel height ignored the cell, which made the\n\t\t\t\t// banner un-resizable and made it overflow/scroll inside a\n\t\t\t\t// smaller cell. The grid controls the size now.\n\t\t\t\theight: '100%',\n\t\t\t\toverflow: 'hidden',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\tstyle['background-image'] = `url(\"${this.backgroundImageUrl}\")`\n\t\t\t\tstyle['background-size'] = 'cover'\n\t\t\t\tstyle['background-position'] = 'center center'\n\t\t\t\tstyle['background-repeat'] = 'no-repeat'\n\t\t\t}\n\t\t\treturn style\n\t\t},\n\n\t\t/** Inline style for the overlay layer. */\n\t\toverlayStyle() {\n\t\t\tif (this.overlayMode === 'gradient-bottom') {\n\t\t\t\treturn {\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\tinset: 0,\n\t\t\t\t\t'background-image': `linear-gradient(to bottom, transparent 50%, ${this.overlayColor} 100%)`,\n\t\t\t\t\t'pointer-events': 'none',\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tposition: 'absolute',\n\t\t\t\tinset: 0,\n\t\t\t\t'background-color': this.overlayColor,\n\t\t\t\topacity: String(this.overlayOpacity),\n\t\t\t\t'pointer-events': 'none',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for the content flex container.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tcontentStyle() {\n\t\t\treturn {\n\t\t\t\tposition: 'relative',\n\t\t\t\t'z-index': 1,\n\t\t\t\t// Beside a view switch the content shares the row instead of\n\t\t\t\t// taking the whole width (see .cn-header-widget--with-views).\n\t\t\t\twidth: this.viewOptions.length > 0 ? 'auto' : '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'flex-direction': 'column',\n\t\t\t\t'align-items': this.flexAlignFromTextAlign,\n\t\t\t\t'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],\n\t\t\t\t// Plain drops the coloured background, not the card padding:\n\t\t\t\t// at 0 the greeting sat flush against the card's left edge.\n\t\t\t\t// Ground has no card, so it has no padding either.\n\t\t\t\tpadding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),\n\t\t\t\t'box-sizing': 'border-box',\n\t\t\t\tgap: this.isGround ? '6px' : '8px',\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t}\n\t\t},\n\n\t\t/** Flex cross-axis alignment derived from the text alignment. */\n\t\tflexAlignFromTextAlign() {\n\t\t\tif (this.textAlign === 'left') {\n\t\t\t\treturn 'flex-start'\n\t\t\t}\n\t\t\tif (this.textAlign === 'right') {\n\t\t\t\treturn 'flex-end'\n\t\t\t}\n\t\t\treturn 'center'\n\t\t},\n\n\t\t/** Inline style applying the resolved text colour. */\n\t\ttextStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** The validated CTA object, or null when incomplete. */\n\t\tcta() {\n\t\t\tconst raw = this.content && this.content.cta\n\t\t\tif (raw === null || typeof raw !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = typeof raw.label === 'string' ? raw.label.trim() : ''\n\t\t\tconst url = typeof raw.url === 'string' ? raw.url.trim() : ''\n\t\t\tif (label === '' || url === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet style = raw.style\n\t\t\tif (typeof style !== 'string' || ALLOWED_CTA_STYLES.includes(style) === false) {\n\t\t\t\tstyle = 'primary'\n\t\t\t}\n\t\t\treturn { label, url, style }\n\t\t},\n\n\t\t/** Whether a complete CTA is present. */\n\t\thasCta() {\n\t\t\treturn this.cta !== null\n\t\t},\n\n\t\t/** The CTA label, translated. */\n\t\tctaLabel() {\n\t\t\treturn this.hasCta && this.cta.label ? this.effectiveTranslate(this.cta.label) : ''\n\t\t},\n\n\t\t/** The CTA URL. */\n\t\tctaUrl() {\n\t\t\treturn this.hasCta ? this.cta.url : ''\n\t\t},\n\n\t\t/** Whether the CTA URL is an external http(s) link. */\n\t\tctaIsExternal() {\n\t\t\treturn this.hasCta && /^https?:\\/\\//i.test(this.ctaUrl)\n\t\t},\n\n\t\t/** The CTA anchor target (`_blank` for external links). */\n\t\tctaTarget() {\n\t\t\treturn this.ctaIsExternal ? '_blank' : null\n\t\t},\n\n\t\t/** The CTA anchor rel for external links. */\n\t\tctaRel() {\n\t\t\treturn this.ctaIsExternal ? 'noopener noreferrer' : null\n\t\t},\n\n\t\t/** The accessible CTA label, annotating external links. */\n\t\tctaAriaLabel() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.ctaIsExternal) {\n\t\t\t\treturn `${this.ctaLabel} (${t('nextcloud-vue', 'opens in new tab')})`\n\t\t\t}\n\t\t\treturn this.ctaLabel\n\t\t},\n\n\t\t/** The CTA class list (base + style modifier). */\n\t\tctaClasses() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t'cn-header-widget__cta',\n\t\t\t\t`cn-header-widget__cta--${this.cta.style}`,\n\t\t\t]\n\t\t},\n\t},\n\n\twatch: {\n\t\tbackgroundImageUrl() {\n\t\t\t// Re-arm the probe so a previously failing URL can recover when\n\t\t\t// the user edits the placement.\n\t\t\tthis.imageFailed = false\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Follow the chosen view: push its route (with the option's `params`\n\t\t * when declared). Nothing happens for the route already shown.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @param {string} route The chosen option's route name.\n\t\t * @return {void}\n\t\t */\n\t\tonViewChange(route) {\n\t\t\tif (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {\n\t\t\t\tif (this.cnPageViews && typeof this.cnPageViews.select === 'function') {\n\t\t\t\t\tthis.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.$router || !route || route === (this.$route && this.$route.name)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}\n\t\t\tconst target = { name: route }\n\t\t\tif (declared.params && typeof declared.params === 'object') {\n\t\t\t\ttarget.params = declared.params\n\t\t\t}\n\t\t\tthis.$router.push(target)\n\t\t},\n\n\t\t/**\n\t\t * Mark the background image as broken so the renderer falls back to\n\t\t * the solid colour.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonImageError() {\n\t\t\tthis.imageFailed = true\n\t\t},\n\n\t\t/**\n\t\t * Quick perceived-luminance check on a CSS hex colour. Returns true\n\t\t * for \"light\" (white text would be illegible). Non-hex colours\n\t\t * collapse to false (assume dark) — the safer default.\n\t\t *\n\t\t * @param {string} color the CSS colour value.\n\t\t * @return {boolean} true when the colour is perceived as light.\n\t\t */\n\t\tisLightColor(color) {\n\t\t\tif (typeof color !== 'string') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst match = color.trim().match(/^#([0-9a-f]{6})$/i)\n\t\t\tif (!match) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst hex = match[1]\n\t\t\tconst r = Number.parseInt(hex.slice(0, 2), 16)\n\t\t\tconst g = Number.parseInt(hex.slice(2, 4), 16)\n\t\t\tconst b = Number.parseInt(hex.slice(4, 6), 16)\n\t\t\tconst luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255\n\t\t\treturn luma > 0.6\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-header-widget {\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 120px;\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n.cn-header-widget--plain {\n\tmin-height: 0;\n\tborder-radius: 0;\n}\n\n/* With a view switch the heading and the switch share one row, the switch\n at the right and bottom-aligned with the heading; on a narrow card the\n switch wraps under it. */\n.cn-header-widget--with-views {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tjustify-content: space-between;\n\tgap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));\n}\n\n.cn-header-widget--with-views .cn-header-widget__content {\n\tflex: 1 1 320px;\n\twidth: auto;\n}\n\n.cn-header-widget__views {\n\tposition: relative;\n\tz-index: 1;\n\tflex: 0 0 auto;\n\tpadding: 16px;\n}\n\n/* On the page ground (`content.ground`): no card, so no inset around the\n view switch, and the heading at the board's 32px. */\n.cn-header-widget--ground .cn-header-widget__views {\n\tpadding: 0;\n}\n\n.cn-header-widget--ground .cn-header-widget__title {\n\tfont-size: var(--cn-header-ground-title-size, 32px);\n\tletter-spacing: -0.01em;\n}\n\n.cn-header-widget__date {\n\tfont-size: 14px;\n\tline-height: 1.4;\n\tmargin: 0;\n}\n\n.cn-header-widget__overlay {\n\tposition: absolute;\n\tinset: 0;\n}\n\n.cn-header-widget__content {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-header-widget__title {\n\tfont-size: 28px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tmargin: 0;\n}\n\n.cn-header-widget__subtitle {\n\tfont-size: 16px;\n\tline-height: 1.4;\n\tmargin: 0;\n\topacity: 0.95;\n}\n\n.cn-header-widget__cta {\n\tdisplay: inline-block;\n\tmargin-top: 8px;\n\tpadding: 10px 18px;\n\tborder-radius: var(--border-radius, 4px);\n\tfont-size: 14px;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n\ttransition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.cn-header-widget__cta:hover {\n\ttransform: translateY(-1px);\n\tbox-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\n\n.cn-header-widget__cta--primary {\n\tbackground-color: var(--color-primary, #0070c0);\n\tcolor: var(--color-primary-text, #ffffff);\n\tborder: 1px solid var(--color-primary, #0070c0);\n}\n\n.cn-header-widget__cta--secondary {\n\tbackground-color: var(--color-background-hover, rgba(255, 255, 255, 0.85));\n\tcolor: var(--color-main-text, #000000);\n\tborder: 1px solid var(--color-border, #cccccc);\n}\n\n.cn-header-widget__cta--ghost {\n\tbackground-color: transparent;\n\tcolor: inherit;\n\tborder: 1px solid currentColor;\n}\n\n.cn-header-widget__probe {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\topacity: 0;\n\tpointer-events: none;\n\tinset-inline-start: -9999px;\n\ttop: -9999px;\n}\n\n@media print {\n\t.cn-header-widget {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":["CnSegmentedControl","t"],"mappings":"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"}
|
|
@@ -65,9 +65,10 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
65
65
|
options: $options.viewOptions,
|
|
66
66
|
modelValue: $options.activeView,
|
|
67
67
|
ariaLabel: $options.viewsLabel,
|
|
68
|
+
controls: $options.viewsRegionId,
|
|
68
69
|
"data-testid": "cn-header-widget-views",
|
|
69
70
|
"onUpdate:modelValue": $options.onViewChange
|
|
70
|
-
}, null, 8 /* PROPS */, ["options", "modelValue", "ariaLabel", "onUpdate:modelValue"])
|
|
71
|
+
}, null, 8 /* PROPS */, ["options", "modelValue", "ariaLabel", "controls", "onUpdate:modelValue"])
|
|
71
72
|
]))
|
|
72
73
|
: createCommentVNode("v-if", true),
|
|
73
74
|
($options.hasBackgroundImage)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnHeaderWidget.vue3.js","sources":["../../../../src/components/CnHeaderWidget/CnHeaderWidget.vue?vue&type=template&id=1b97430f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<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\">\n\t\t<div\n\t\t\tv-if=\"hasOverlay\"\n\t\t\tclass=\"cn-header-widget__overlay\"\n\t\t\t:style=\"overlayStyle\"\n\t\t\taria-hidden=\"true\" />\n\t\t<div class=\"cn-header-widget__content\" :style=\"contentStyle\">\n\t\t\t<p\n\t\t\t\tv-if=\"dateLine\"\n\t\t\t\tclass=\"cn-header-widget__date\"\n\t\t\t\t:style=\"dateStyle\"\n\t\t\t\tdata-testid=\"cn-header-widget-date\">\n\t\t\t\t{{ dateLine }}\n\t\t\t</p>\n\t\t\t<h2 v-if=\"hasTitle\" class=\"cn-header-widget__title\" :style=\"textStyle\">\n\t\t\t\t{{ headingText }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"hasSubtitle\" class=\"cn-header-widget__subtitle\" :style=\"textStyle\">\n\t\t\t\t{{ subtitle }}\n\t\t\t</p>\n\t\t\t<a\n\t\t\t\tv-if=\"hasCta\"\n\t\t\t\t:href=\"ctaUrl\"\n\t\t\t\t:class=\"ctaClasses\"\n\t\t\t\t:target=\"ctaTarget\"\n\t\t\t\t:rel=\"ctaRel\"\n\t\t\t\t:aria-label=\"ctaAriaLabel\">\n\t\t\t\t{{ ctaLabel }}\n\t\t\t</a>\n\t\t</div>\n\t\t<!-- A view switch at the right of the heading (`content.views`): \"My\n\t\t work / My team\" as a segmented control whose options are routes. -->\n\t\t<div v-if=\"viewOptions.length > 0\" class=\"cn-header-widget__views\">\n\t\t\t<CnSegmentedControl\n\t\t\t\t:options=\"viewOptions\"\n\t\t\t\t:modelValue=\"activeView\"\n\t\t\t\t:ariaLabel=\"viewsLabel\"\n\t\t\t\tdata-testid=\"cn-header-widget-views\"\n\t\t\t\t@update:modelValue=\"onViewChange\" />\n\t\t</div>\n\t\t<img\n\t\t\tv-if=\"hasBackgroundImage\"\n\t\t\tclass=\"cn-header-widget__probe\"\n\t\t\t:src=\"backgroundImageUrl\"\n\t\t\talt=\"\"\n\t\t\taria-hidden=\"true\"\n\t\t\t@error=\"onImageError\">\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { getCanonicalLocale, translate as t } from '@nextcloud/l10n'\nimport CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'\nimport { resolveImageUrl } from '../../utils/resolveImageUrl.js'\n\nconst ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']\nconst ALLOWED_HEIGHTS = ['small', 'medium', 'large', 'xlarge']\nconst ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']\nconst ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']\nconst ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']\n\nconst HEIGHT_PIXELS = Object.freeze({\n\tsmall: 120,\n\tmedium: 200,\n\tlarge: 320,\n\txlarge: 480,\n})\n\nconst VERTICAL_ALIGN_FLEX = Object.freeze({\n\ttop: 'flex-start',\n\tmiddle: 'center',\n\tbottom: 'flex-end',\n})\n\n/**\n * CnHeaderWidget — full-width banner widget for dashboards. Renders a\n * configurable header with title, optional subtitle, optional background\n * image, optional colour overlay, and optional call-to-action button.\n *\n * Image source precedence: `backgroundImageFileId` takes priority over\n * `backgroundImageUrl` (resolved via the Nextcloud core preview route). When\n * the image fails to load the renderer silently falls back to the solid\n * `backgroundColor`; no error UI is shown.\n *\n * Height presets map to fixed pixels: small=120, medium=200, large=320,\n * xlarge=480. Title is `<h2>`, subtitle `<p>`, CTA `<a>` — all semantic HTML.\n * External CTA URLs (http/https) open in a new tab with\n * `rel=\"noopener noreferrer\"`; relative / anchor URLs stay in the current tab.\n *\n * Optional greeting: `greeting: true` makes the heading greet the signed-in\n * user by time of day and first name (\"Good afternoon, Pieter\"; `\"full\"` uses\n * the whole display name), and `showDate: true` adds a line with today's\n * date above it. `plain: true` drops the coloured background and aligns the\n * text to the start, which is how a greeting usually sits on a dashboard.\n *\n * ```json\n * { \"widgetKey\": \"header\", \"props\": { \"content\": { \"greeting\": true, \"showDate\": true, \"plain\": true } } }\n * ```\n *\n * Registered as the `header` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnHeaderWidget',\n\n\tcomponents: { CnSegmentedControl },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.title` / `content.subtitle` / CTA label\n\t\t * are run through it. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content: `{title, subtitle, backgroundImageUrl,\n\t\t * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,\n\t\t * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,\n\t\t * greeting, showDate, plain, ground, views}`. All fields are optional except\n\t\t * `title` (or `greeting`); unknown enum values collapse to documented\n\t\t * defaults and the renderer never throws. `views` is\n\t\t * `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented\n\t\t * control at the right of the heading whose checked option is the\n\t\t * current route; choosing another pushes its route. `ground: true`\n\t\t * draws the greeting on the page ground: the plain look with no\n\t\t * padding, a 32px heading and the date line 6px above it (a dashboard\n\t\t * also drops the widget's card for it).\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/* eslint-disable vue/no-unused-properties -- part of the widget contract: CnContainerChild and the dashboard pages bind :placement on every widget, so declaring it keeps it out of $attrs */\n\t\t/**\n\t\t * Placement entity — reserved to match the renderer contract.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/**\n\t\t * The moment the greeting and the date line are computed for. Leave\n\t\t * empty for the current time.\n\t\t *\n\t\t * @type {Date|null}\n\t\t */\n\t\tnow: {\n\t\t\ttype: Date,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t// Set true once the background image fires a DOM `error` so\n\t\t\t// subsequent renders fall back to `backgroundColor` only. The\n\t\t\t// probe `<img>` is invisible — its only job is load-failure\n\t\t\t// detection for the CSS background.\n\t\t\timageFailed: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the injected `cnTranslate` (the host\n\t\t * app's bound `t()`), identity by default.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t},\n\n\t\t/** The header title, translated, or '' when absent. */\n\t\ttitle() {\n\t\t\tconst value = this.content && this.content.title\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** The header subtitle, translated, or '' when absent. */\n\t\tsubtitle() {\n\t\t\tconst value = this.content && this.content.subtitle\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** Whether a heading renders: a non-empty title or a greeting. */\n\t\thasTitle() {\n\t\t\treturn this.headingText !== ''\n\t\t},\n\n\t\t/**\n\t\t * The moment the greeting and date line are computed for.\n\t\t *\n\t\t * @return {Date}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\treference() {\n\t\t\treturn this.now instanceof Date ? this.now : new Date()\n\t\t},\n\n\t\t/**\n\t\t * The view switch's options from `content.views.options`: each\n\t\t * `{ label, route, params? }` with a non-empty label and route becomes\n\t\t * a segmented-control option keyed on its route name (the label goes\n\t\t * through the host translate function). Empty without a router, so\n\t\t * the switch cannot offer routes it cannot follow.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewOptions() {\n\t\t\tconst raw = this.content && this.content.views && this.content.views.options\n\t\t\tif (!Array.isArray(raw) || !this.$router) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn raw\n\t\t\t\t.filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')\n\t\t\t\t.map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))\n\t\t},\n\n\t\t/**\n\t\t * The checked view: the option whose route is the current route, else\n\t\t * the first option (a switch always has one checked option).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string|null}\n\t\t */\n\t\tactiveView() {\n\t\t\tconst options = this.viewOptions\n\t\t\tif (options.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst current = this.$route && this.$route.name\n\t\t\tconst match = options.find((o) => o.value === current)\n\t\t\treturn match ? match.value : options[0].value\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch: `content.views.ariaLabel`\n\t\t * through the host translate function, else \"View\".\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string}\n\t\t */\n\t\tviewsLabel() {\n\t\t\tconst label = this.content && this.content.views && this.content.views.ariaLabel\n\t\t\treturn label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')\n\t\t},\n\n\t\t/**\n\t\t * Whether the plain presentation is on (no coloured background).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisPlain() {\n\t\t\treturn Boolean(this.content && (this.content.plain === true || this.content.ground === true))\n\t\t},\n\n\t\t/**\n\t\t * Whether the greeting sits on the page ground (`content.ground`):\n\t\t * the plain look without the card padding, so the date line and the\n\t\t * heading align with the page edge as the board draws them. Off by\n\t\t * default.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground\n\t\t * @return {boolean}\n\t\t */\n\t\tisGround() {\n\t\t\treturn Boolean(this.content && this.content.ground === true)\n\t\t},\n\n\t\t/**\n\t\t * The name the greeting uses: the first word of the display name, or\n\t\t * the whole display name for `greeting: \"full\"`. '' when unknown.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingName() {\n\t\t\tlet user\n\t\t\ttry {\n\t\t\t\tuser = getCurrentUser()\n\t\t\t} catch {\n\t\t\t\tuser = null\n\t\t\t}\n\t\t\tconst displayName = (user && typeof user.displayName === 'string') ? user.displayName.trim() : ''\n\t\t\tif (displayName === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.content.greeting === 'full' ? displayName : displayName.split(/\\s+/)[0]\n\t\t},\n\n\t\t/**\n\t\t * The greeting for the time of day, with the user's name when known,\n\t\t * or '' when the greeting is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingText() {\n\t\t\tconst greeting = this.content && this.content.greeting\n\t\t\tif (greeting !== true && greeting !== 'full' && greeting !== 'first') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst hour = this.reference.getHours()\n\t\t\tconst name = this.greetingName\n\t\t\tif (hour < 12) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good morning, {name}', { name }) : t('nextcloud-vue', 'Good morning')\n\t\t\t}\n\t\t\tif (hour < 18) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good afternoon, {name}', { name }) : t('nextcloud-vue', 'Good afternoon')\n\t\t\t}\n\t\t\treturn name ? t('nextcloud-vue', 'Good evening, {name}', { name }) : t('nextcloud-vue', 'Good evening')\n\t\t},\n\n\t\t/**\n\t\t * The heading: the greeting when it is on, else the title.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\theadingText() {\n\t\t\treturn this.greetingText || this.title\n\t\t},\n\n\t\t/**\n\t\t * Today's date written out (\"Monday 5 October 2026\"), or '' when\n\t\t * `showDate` is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateLine() {\n\t\t\tif (!this.content || this.content.showDate !== true) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }\n\t\t\tlet locale\n\t\t\ttry {\n\t\t\t\tlocale = getCanonicalLocale()\n\t\t\t} catch {\n\t\t\t\tlocale = undefined\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale || undefined, options).format(this.reference)\n\t\t\t} catch {\n\t\t\t\treturn new Intl.DateTimeFormat(undefined, options).format(this.reference)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style of the date line: the heading's colour, or the muted\n\t\t * text colour on a plain header.\n\t\t *\n\t\t * @return {object}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.isPlain && !(this.content && this.content.textColor) ? 'var(--color-text-maxcontrast)' : this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** Whether a non-empty subtitle is set. */\n\t\thasSubtitle() {\n\t\t\treturn this.subtitle !== ''\n\t\t},\n\n\t\t/** Resolved background image URL (file id wins over URL). */\n\t\tbackgroundImageUrl() {\n\t\t\tconst fileId = this.content && this.content.backgroundImageFileId\n\t\t\tif (typeof fileId === 'number' && Number.isFinite(fileId) && fileId > 0) {\n\t\t\t\treturn `/index.php/core/preview?fileId=${fileId}&x=1024&y=512&a=1`\n\t\t\t}\n\t\t\tconst url = this.content && this.content.backgroundImageUrl\n\t\t\t// Accept http(s) plus uploaded/same-origin sources: data: and blob:\n\t\t\t// (from the form's upload) and root-relative paths (NC files/preview).\n\t\t\t// External http(s) images may still be blocked by the instance CSP —\n\t\t\t// uploading is the reliable, same-origin path. resolveImageUrl() adds\n\t\t\t// the webroot + /index.php to `/apps/...` resource paths so they load\n\t\t\t// on index.php-routed instances (other shapes pass through).\n\t\t\tif (typeof url === 'string' && /^(https?:\\/\\/|data:|blob:|\\/)/i.test(url)) {\n\t\t\t\treturn resolveImageUrl(url)\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/** Whether a usable, non-errored background image is present. */\n\t\thasBackgroundImage() {\n\t\t\treturn this.backgroundImageUrl !== '' && this.imageFailed === false\n\t\t},\n\n\t\t/**\n\t\t * Resolved background colour (default theme primary).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tbackgroundColor() {\n\t\t\tconst value = this.content && this.content.backgroundColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.isPlain) {\n\t\t\t\treturn 'transparent'\n\t\t\t}\n\t\t\treturn 'var(--color-primary, #0070c0)'\n\t\t},\n\n\t\t/** Resolved overlay mode (`tint` when an image is present, else `none`). */\n\t\toverlayMode() {\n\t\t\tconst declared = this.content && this.content.overlayMode\n\t\t\tif (typeof declared === 'string' && ALLOWED_OVERLAY_MODES.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.hasBackgroundImage ? 'tint' : 'none'\n\t\t},\n\n\t\t/** Whether an overlay should render. */\n\t\thasOverlay() {\n\t\t\treturn this.overlayMode !== 'none'\n\t\t},\n\n\t\t/** Resolved overlay colour. */\n\t\toverlayColor() {\n\t\t\tconst value = this.content && this.content.overlayColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst bg = this.content && this.content.backgroundColor\n\t\t\treturn (typeof bg === 'string' && bg !== '') ? bg : '#000000'\n\t\t},\n\n\t\t/** Resolved overlay opacity, clamped to 0..1 (default 0.4). */\n\t\toverlayOpacity() {\n\t\t\tconst raw = this.content && this.content.overlayOpacity\n\t\t\tconst value = typeof raw === 'number' ? raw : Number.parseFloat(raw)\n\t\t\tif (Number.isFinite(value) === false) {\n\t\t\t\treturn 0.4\n\t\t\t}\n\t\t\tif (value < 0) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tif (value > 1) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/** Resolved height preset key (default `medium`). */\n\t\theight() {\n\t\t\tconst declared = this.content && this.content.height\n\t\t\tif (typeof declared === 'string' && ALLOWED_HEIGHTS.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'medium'\n\t\t},\n\n\t\t/** The pixel height for the active preset. */\n\t\theightPixels() {\n\t\t\treturn HEIGHT_PIXELS[this.height]\n\t\t},\n\n\t\t/**\n\t\t * Resolved text alignment (default `center`).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextAlign() {\n\t\t\tconst declared = this.content && this.content.textAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_TEXT_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.isPlain ? 'left' : 'center'\n\t\t},\n\n\t\t/** Resolved vertical alignment (default `middle`). */\n\t\tverticalAlign() {\n\t\t\tconst declared = this.content && this.content.verticalAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_VERTICAL_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'middle'\n\t\t},\n\n\t\t/**\n\t\t * Resolved text colour with an auto-contrast fallback.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextColor() {\n\t\t\tconst value = this.content && this.content.textColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\treturn '#ffffff'\n\t\t\t}\n\t\t\tif (this.isPlain && !(this.content && this.content.backgroundColor)) {\n\t\t\t\treturn 'var(--color-main-text)'\n\t\t\t}\n\t\t\treturn this.isLightColor(this.backgroundColor) ? '#000000' : '#ffffff'\n\t\t},\n\n\t\t/** Inline style for the banner wrapper. */\n\t\twrapperStyle() {\n\t\t\tconst style = {\n\t\t\t\tposition: 'relative',\n\t\t\t\twidth: '100%',\n\t\t\t\t// Fill the dashboard grid cell so the banner resizes with the\n\t\t\t\t// widget. A fixed pixel height ignored the cell, which made the\n\t\t\t\t// banner un-resizable and made it overflow/scroll inside a\n\t\t\t\t// smaller cell. The grid controls the size now.\n\t\t\t\theight: '100%',\n\t\t\t\toverflow: 'hidden',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\tstyle['background-image'] = `url(\"${this.backgroundImageUrl}\")`\n\t\t\t\tstyle['background-size'] = 'cover'\n\t\t\t\tstyle['background-position'] = 'center center'\n\t\t\t\tstyle['background-repeat'] = 'no-repeat'\n\t\t\t}\n\t\t\treturn style\n\t\t},\n\n\t\t/** Inline style for the overlay layer. */\n\t\toverlayStyle() {\n\t\t\tif (this.overlayMode === 'gradient-bottom') {\n\t\t\t\treturn {\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\tinset: 0,\n\t\t\t\t\t'background-image': `linear-gradient(to bottom, transparent 50%, ${this.overlayColor} 100%)`,\n\t\t\t\t\t'pointer-events': 'none',\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tposition: 'absolute',\n\t\t\t\tinset: 0,\n\t\t\t\t'background-color': this.overlayColor,\n\t\t\t\topacity: String(this.overlayOpacity),\n\t\t\t\t'pointer-events': 'none',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for the content flex container.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tcontentStyle() {\n\t\t\treturn {\n\t\t\t\tposition: 'relative',\n\t\t\t\t'z-index': 1,\n\t\t\t\t// Beside a view switch the content shares the row instead of\n\t\t\t\t// taking the whole width (see .cn-header-widget--with-views).\n\t\t\t\twidth: this.viewOptions.length > 0 ? 'auto' : '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'flex-direction': 'column',\n\t\t\t\t'align-items': this.flexAlignFromTextAlign,\n\t\t\t\t'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],\n\t\t\t\t// Plain drops the coloured background, not the card padding:\n\t\t\t\t// at 0 the greeting sat flush against the card's left edge.\n\t\t\t\t// Ground has no card, so it has no padding either.\n\t\t\t\tpadding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),\n\t\t\t\t'box-sizing': 'border-box',\n\t\t\t\tgap: this.isGround ? '6px' : '8px',\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t}\n\t\t},\n\n\t\t/** Flex cross-axis alignment derived from the text alignment. */\n\t\tflexAlignFromTextAlign() {\n\t\t\tif (this.textAlign === 'left') {\n\t\t\t\treturn 'flex-start'\n\t\t\t}\n\t\t\tif (this.textAlign === 'right') {\n\t\t\t\treturn 'flex-end'\n\t\t\t}\n\t\t\treturn 'center'\n\t\t},\n\n\t\t/** Inline style applying the resolved text colour. */\n\t\ttextStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** The validated CTA object, or null when incomplete. */\n\t\tcta() {\n\t\t\tconst raw = this.content && this.content.cta\n\t\t\tif (raw === null || typeof raw !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = typeof raw.label === 'string' ? raw.label.trim() : ''\n\t\t\tconst url = typeof raw.url === 'string' ? raw.url.trim() : ''\n\t\t\tif (label === '' || url === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet style = raw.style\n\t\t\tif (typeof style !== 'string' || ALLOWED_CTA_STYLES.includes(style) === false) {\n\t\t\t\tstyle = 'primary'\n\t\t\t}\n\t\t\treturn { label, url, style }\n\t\t},\n\n\t\t/** Whether a complete CTA is present. */\n\t\thasCta() {\n\t\t\treturn this.cta !== null\n\t\t},\n\n\t\t/** The CTA label, translated. */\n\t\tctaLabel() {\n\t\t\treturn this.hasCta && this.cta.label ? this.effectiveTranslate(this.cta.label) : ''\n\t\t},\n\n\t\t/** The CTA URL. */\n\t\tctaUrl() {\n\t\t\treturn this.hasCta ? this.cta.url : ''\n\t\t},\n\n\t\t/** Whether the CTA URL is an external http(s) link. */\n\t\tctaIsExternal() {\n\t\t\treturn this.hasCta && /^https?:\\/\\//i.test(this.ctaUrl)\n\t\t},\n\n\t\t/** The CTA anchor target (`_blank` for external links). */\n\t\tctaTarget() {\n\t\t\treturn this.ctaIsExternal ? '_blank' : null\n\t\t},\n\n\t\t/** The CTA anchor rel for external links. */\n\t\tctaRel() {\n\t\t\treturn this.ctaIsExternal ? 'noopener noreferrer' : null\n\t\t},\n\n\t\t/** The accessible CTA label, annotating external links. */\n\t\tctaAriaLabel() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.ctaIsExternal) {\n\t\t\t\treturn `${this.ctaLabel} (${t('nextcloud-vue', 'opens in new tab')})`\n\t\t\t}\n\t\t\treturn this.ctaLabel\n\t\t},\n\n\t\t/** The CTA class list (base + style modifier). */\n\t\tctaClasses() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t'cn-header-widget__cta',\n\t\t\t\t`cn-header-widget__cta--${this.cta.style}`,\n\t\t\t]\n\t\t},\n\t},\n\n\twatch: {\n\t\tbackgroundImageUrl() {\n\t\t\t// Re-arm the probe so a previously failing URL can recover when\n\t\t\t// the user edits the placement.\n\t\t\tthis.imageFailed = false\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Follow the chosen view: push its route (with the option's `params`\n\t\t * when declared). Nothing happens for the route already shown.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @param {string} route The chosen option's route name.\n\t\t * @return {void}\n\t\t */\n\t\tonViewChange(route) {\n\t\t\tif (!this.$router || !route || route === (this.$route && this.$route.name)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}\n\t\t\tconst target = { name: route }\n\t\t\tif (declared.params && typeof declared.params === 'object') {\n\t\t\t\ttarget.params = declared.params\n\t\t\t}\n\t\t\tthis.$router.push(target)\n\t\t},\n\n\t\t/**\n\t\t * Mark the background image as broken so the renderer falls back to\n\t\t * the solid colour.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonImageError() {\n\t\t\tthis.imageFailed = true\n\t\t},\n\n\t\t/**\n\t\t * Quick perceived-luminance check on a CSS hex colour. Returns true\n\t\t * for \"light\" (white text would be illegible). Non-hex colours\n\t\t * collapse to false (assume dark) — the safer default.\n\t\t *\n\t\t * @param {string} color the CSS colour value.\n\t\t * @return {boolean} true when the colour is perceived as light.\n\t\t */\n\t\tisLightColor(color) {\n\t\t\tif (typeof color !== 'string') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst match = color.trim().match(/^#([0-9a-f]{6})$/i)\n\t\t\tif (!match) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst hex = match[1]\n\t\t\tconst r = Number.parseInt(hex.slice(0, 2), 16)\n\t\t\tconst g = Number.parseInt(hex.slice(2, 4), 16)\n\t\t\tconst b = Number.parseInt(hex.slice(4, 6), 16)\n\t\t\tconst luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255\n\t\t\treturn luma > 0.6\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-header-widget {\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 120px;\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n.cn-header-widget--plain {\n\tmin-height: 0;\n\tborder-radius: 0;\n}\n\n/* With a view switch the heading and the switch share one row, the switch\n at the right and bottom-aligned with the heading; on a narrow card the\n switch wraps under it. */\n.cn-header-widget--with-views {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tjustify-content: space-between;\n\tgap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));\n}\n\n.cn-header-widget--with-views .cn-header-widget__content {\n\tflex: 1 1 320px;\n\twidth: auto;\n}\n\n.cn-header-widget__views {\n\tposition: relative;\n\tz-index: 1;\n\tflex: 0 0 auto;\n\tpadding: 16px;\n}\n\n/* On the page ground (`content.ground`): no card, so no inset around the\n view switch, and the heading at the board's 32px. */\n.cn-header-widget--ground .cn-header-widget__views {\n\tpadding: 0;\n}\n\n.cn-header-widget--ground .cn-header-widget__title {\n\tfont-size: var(--cn-header-ground-title-size, 32px);\n\tletter-spacing: -0.01em;\n}\n\n.cn-header-widget__date {\n\tfont-size: 14px;\n\tline-height: 1.4;\n\tmargin: 0;\n}\n\n.cn-header-widget__overlay {\n\tposition: absolute;\n\tinset: 0;\n}\n\n.cn-header-widget__content {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-header-widget__title {\n\tfont-size: 28px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tmargin: 0;\n}\n\n.cn-header-widget__subtitle {\n\tfont-size: 16px;\n\tline-height: 1.4;\n\tmargin: 0;\n\topacity: 0.95;\n}\n\n.cn-header-widget__cta {\n\tdisplay: inline-block;\n\tmargin-top: 8px;\n\tpadding: 10px 18px;\n\tborder-radius: var(--border-radius, 4px);\n\tfont-size: 14px;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n\ttransition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.cn-header-widget__cta:hover {\n\ttransform: translateY(-1px);\n\tbox-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\n\n.cn-header-widget__cta--primary {\n\tbackground-color: var(--color-primary, #0070c0);\n\tcolor: var(--color-primary-text, #ffffff);\n\tborder: 1px solid var(--color-primary, #0070c0);\n}\n\n.cn-header-widget__cta--secondary {\n\tbackground-color: var(--color-background-hover, rgba(255, 255, 255, 0.85));\n\tcolor: var(--color-main-text, #000000);\n\tborder: 1px solid var(--color-border, #cccccc);\n}\n\n.cn-header-widget__cta--ghost {\n\tbackground-color: transparent;\n\tcolor: inherit;\n\tborder: 1px solid currentColor;\n}\n\n.cn-header-widget__probe {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\topacity: 0;\n\tpointer-events: none;\n\tinset-inline-start: -9999px;\n\ttop: -9999px;\n}\n\n@media print {\n\t.cn-header-widget {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createElementVNode","_openBlock","_createVNode"],"mappings":";;;;;AA2CqC,EAAA,KAAE,EAAA,yBAAA;;;;;;;uBAhCpCA;IAAG,KAAK,EAAAC,cAAA,CAAA,CAAC,kBAAW,EAAA,EAAA,yBAAA,EAAA,QAAA,CAAA,OAAA,8BAAA,QAAA,CAAA,QAAA,EAAA,8BAAA,EAAA,QAAA,CAAA,WAAA,CAAA,MAAA,GAAA,CAAA,EAAA,CAAA,CAAA;AAAA,IAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;KAEnB,QAAA,CAAA,UAAA;sBADFD,kBAImB,CAAA,KAAA,EAAA;;AAFjB,UAAA,KAAI,EAAC,2BAAS;AACd,UAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;AACnB,UAAA,aAAU,EAAC,MAAM;;;IACjBE,kBAuBI,CAAA,KAAA,EAAA;AAvBD,MAAA,KAAK,EAAC,2BAA0B;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;OAEjC,QAAA,CAAA,QAAA;0CAKC,CAAA,GAAA,EAAA;;AAJH,YAAA,KAAK,EAAC,wBAAwB;AAC7B,YAAA,KAAK,iBAAE,QAAQ,CAAA,SAAA,CAAA;AAChB,YAAA,aAAG,EAAA,uBAAA;6BACA,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;OAET,QAAA,CAAA,QAAA;wBAAHF,kBAEK,CAAA,IAAA,EAAA;;AAFF,YAAA,KAAA,EAAA,yBAAA;AAAA,YAAA,KAAA,iBAAA,QAAA,CAAA,SAAA,CAAA;6BACF,QAAA,CAAA,WAAA,CAAA,EAAA,CAAA,mBAAA;;OAEQ,QAAK,CAAA,WAAA;0CAEV,CAAA,GAAA,EAAA;;AAFU,YAAA,KAAA,EAAA,4BAAA;AAAA,YAAA,KAAA,iBAAA,QAAA,CAAA,SAAA,CAAA;6BACV,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;OAGL,QAAM,CAAA,MAAA;0CAOT,CAAA,GAAA,EAAA;;AANF,YAAA,IAAI,EAAE,QAAM,CAAA,MAAA;AACZ,YAAA,KAAC,iBAAA,QAAA,CAAA,UAAA,CAAA;AACD,YAAA,MAAE,EAAA,QAAA,CAAA,SAAA;AACF,YAAA,GAAI,EAAC,QAAM,CAAA,MAAA;wBACD,EAAE,QAAY,CAAA,YAAA;6BACtB,QAAQ,CAAA,QAAA,CAAA,EAAA,EAAA,2BAAA,UAAA,CAAA;;;AAKF,IAAA,CAAA,QAAA,CAAA,WAAW,CAAC,MAAM,GAAA,CAAA;AAA3B,SAAAG,SAAA,EAAA,EAAAH,kBAAA,CAOI,OAPJ,UAOI,EAAA;UANJI,WAKuB,CAAA,6BAAA,EAAA;AAJtB,YAAA,OAAE,EAAA,QAAA,CAAA,WAAA;AACF,YAAA,UAAA,EAAA,QAAA,CAAA,UAAA;AACA,YAAA,SAAS,EAAE,QAAU,CAAA,UAAA;AACtB,YAAA,aAAW,EAAC,wBAAiB;AAC5B,YAAA,qBAAiB,EAAE,QAAG,CAAA,YAAA;;;;KAGlB,QAAe,CAAA,kBAAA;sBADrBJ;;AAEA,UAAA,KAAG,EAAA,yBAAA;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnHeaderWidget.vue3.js","sources":["../../../../src/components/CnHeaderWidget/CnHeaderWidget.vue?vue&type=template&id=1b97430f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<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\">\n\t\t<div\n\t\t\tv-if=\"hasOverlay\"\n\t\t\tclass=\"cn-header-widget__overlay\"\n\t\t\t:style=\"overlayStyle\"\n\t\t\taria-hidden=\"true\" />\n\t\t<div class=\"cn-header-widget__content\" :style=\"contentStyle\">\n\t\t\t<p\n\t\t\t\tv-if=\"dateLine\"\n\t\t\t\tclass=\"cn-header-widget__date\"\n\t\t\t\t:style=\"dateStyle\"\n\t\t\t\tdata-testid=\"cn-header-widget-date\">\n\t\t\t\t{{ dateLine }}\n\t\t\t</p>\n\t\t\t<h2 v-if=\"hasTitle\" class=\"cn-header-widget__title\" :style=\"textStyle\">\n\t\t\t\t{{ headingText }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"hasSubtitle\" class=\"cn-header-widget__subtitle\" :style=\"textStyle\">\n\t\t\t\t{{ subtitle }}\n\t\t\t</p>\n\t\t\t<a\n\t\t\t\tv-if=\"hasCta\"\n\t\t\t\t:href=\"ctaUrl\"\n\t\t\t\t:class=\"ctaClasses\"\n\t\t\t\t:target=\"ctaTarget\"\n\t\t\t\t:rel=\"ctaRel\"\n\t\t\t\t:aria-label=\"ctaAriaLabel\">\n\t\t\t\t{{ ctaLabel }}\n\t\t\t</a>\n\t\t</div>\n\t\t<!-- A view switch at the right of the heading (`content.views`): \"My\n\t\t work / My team\" as a segmented control whose options are routes,\n\t\t or views of the page the widget sits on (`option.view`); a view\n\t\t option controls the page's view region. -->\n\t\t<div v-if=\"viewOptions.length > 0\" class=\"cn-header-widget__views\">\n\t\t\t<CnSegmentedControl\n\t\t\t\t:options=\"viewOptions\"\n\t\t\t\t:modelValue=\"activeView\"\n\t\t\t\t:ariaLabel=\"viewsLabel\"\n\t\t\t\t:controls=\"viewsRegionId\"\n\t\t\t\tdata-testid=\"cn-header-widget-views\"\n\t\t\t\t@update:modelValue=\"onViewChange\" />\n\t\t</div>\n\t\t<img\n\t\t\tv-if=\"hasBackgroundImage\"\n\t\t\tclass=\"cn-header-widget__probe\"\n\t\t\t:src=\"backgroundImageUrl\"\n\t\t\talt=\"\"\n\t\t\taria-hidden=\"true\"\n\t\t\t@error=\"onImageError\">\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { getCanonicalLocale, translate as t } from '@nextcloud/l10n'\nimport CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'\nimport { resolveImageUrl } from '../../utils/resolveImageUrl.js'\n\nconst ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']\nconst ALLOWED_HEIGHTS = ['small', 'medium', 'large', 'xlarge']\nconst ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']\nconst ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']\nconst ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']\n\n/** Marks a switch option that selects a page view rather than a route. */\nconst VIEW_OPTION_PREFIX = 'view:'\n\nconst HEIGHT_PIXELS = Object.freeze({\n\tsmall: 120,\n\tmedium: 200,\n\tlarge: 320,\n\txlarge: 480,\n})\n\nconst VERTICAL_ALIGN_FLEX = Object.freeze({\n\ttop: 'flex-start',\n\tmiddle: 'center',\n\tbottom: 'flex-end',\n})\n\n/**\n * CnHeaderWidget — full-width banner widget for dashboards. Renders a\n * configurable header with title, optional subtitle, optional background\n * image, optional colour overlay, and optional call-to-action button.\n *\n * Image source precedence: `backgroundImageFileId` takes priority over\n * `backgroundImageUrl` (resolved via the Nextcloud core preview route). When\n * the image fails to load the renderer silently falls back to the solid\n * `backgroundColor`; no error UI is shown.\n *\n * Height presets map to fixed pixels: small=120, medium=200, large=320,\n * xlarge=480. Title is `<h2>`, subtitle `<p>`, CTA `<a>` — all semantic HTML.\n * External CTA URLs (http/https) open in a new tab with\n * `rel=\"noopener noreferrer\"`; relative / anchor URLs stay in the current tab.\n *\n * Optional greeting: `greeting: true` makes the heading greet the signed-in\n * user by time of day and first name (\"Good afternoon, Pieter\"; `\"full\"` uses\n * the whole display name), and `showDate: true` adds a line with today's\n * date above it. `plain: true` drops the coloured background and aligns the\n * text to the start, which is how a greeting usually sits on a dashboard.\n *\n * ```json\n * { \"widgetKey\": \"header\", \"props\": { \"content\": { \"greeting\": true, \"showDate\": true, \"plain\": true } } }\n * ```\n *\n * Registered as the `header` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnHeaderWidget',\n\n\tcomponents: { CnSegmentedControl },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.title` / `content.subtitle` / CTA label\n\t\t * are run through it. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * The views of the page this widget sits on, provided by\n\t\t * CnDashboardPage / CnDetailPage (`views`). An option with a `view`\n\t\t * selects one of them. Null outside such a page.\n\t\t */\n\t\tcnPageViews: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content: `{title, subtitle, backgroundImageUrl,\n\t\t * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,\n\t\t * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,\n\t\t * greeting, showDate, plain, ground, views}`. All fields are optional except\n\t\t * `title` (or `greeting`); unknown enum values collapse to documented\n\t\t * defaults and the renderer never throws. `views` is\n\t\t * `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:\n\t\t * a segmented control at the right of the heading whose checked option\n\t\t * is the current route; choosing another pushes its route. An option\n\t\t * with `view` selects that view of the page the widget sits on (the\n\t\t * page's `views`) instead, and the page then draws no switch of its\n\t\t * own. `ground: true`\n\t\t * draws the greeting on the page ground: the plain look with no\n\t\t * padding, a 32px heading and the date line 6px above it (a dashboard\n\t\t * also drops the widget's card for it).\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/* eslint-disable vue/no-unused-properties -- part of the widget contract: CnContainerChild and the dashboard pages bind :placement on every widget, so declaring it keeps it out of $attrs */\n\t\t/**\n\t\t * Placement entity — reserved to match the renderer contract.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/**\n\t\t * The moment the greeting and the date line are computed for. Leave\n\t\t * empty for the current time.\n\t\t *\n\t\t * @type {Date|null}\n\t\t */\n\t\tnow: {\n\t\t\ttype: Date,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t// Set true once the background image fires a DOM `error` so\n\t\t\t// subsequent renders fall back to `backgroundColor` only. The\n\t\t\t// probe `<img>` is invisible — its only job is load-failure\n\t\t\t// detection for the CSS background.\n\t\t\timageFailed: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the injected `cnTranslate` (the host\n\t\t * app's bound `t()`), identity by default.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t},\n\n\t\t/** The header title, translated, or '' when absent. */\n\t\ttitle() {\n\t\t\tconst value = this.content && this.content.title\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** The header subtitle, translated, or '' when absent. */\n\t\tsubtitle() {\n\t\t\tconst value = this.content && this.content.subtitle\n\t\t\treturn typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : ''\n\t\t},\n\n\t\t/** Whether a heading renders: a non-empty title or a greeting. */\n\t\thasTitle() {\n\t\t\treturn this.headingText !== ''\n\t\t},\n\n\t\t/**\n\t\t * The moment the greeting and date line are computed for.\n\t\t *\n\t\t * @return {Date}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\treference() {\n\t\t\treturn this.now instanceof Date ? this.now : new Date()\n\t\t},\n\n\t\t/**\n\t\t * The view switch's options from `content.views.options`: each\n\t\t * `{ label, route, params? }` with a non-empty label and route becomes\n\t\t * a segmented-control option keyed on its route name (the label goes\n\t\t * through the host translate function). Empty without a router, so\n\t\t * the switch cannot offer routes it cannot follow.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewOptions() {\n\t\t\tconst raw = this.content && this.content.views && this.content.views.options\n\t\t\tif (!Array.isArray(raw)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst pageViewIds = this.pageViewIds\n\t\t\treturn raw\n\t\t\t\t.filter((o) => o && typeof o.label === 'string' && o.label !== '')\n\t\t\t\t.map((o) => {\n\t\t\t\t\tif (typeof o.view === 'string' && o.view !== '') {\n\t\t\t\t\t\treturn pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof o.route === 'string' && o.route !== '' && this.$router) {\n\t\t\t\t\t\treturn { value: o.route, label: this.cnTranslate(o.label) }\n\t\t\t\t\t}\n\t\t\t\t\treturn null\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * The ids of the page's views, empty outside a page with views.\n\t\t *\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 * @return {string[]}\n\t\t */\n\t\tpageViewIds() {\n\t\t\tconst views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : []\n\t\t\treturn views.map((view) => view.id)\n\t\t},\n\n\t\t/**\n\t\t * The id of the page's view region when an option selects a view,\n\t\t * for the options' `aria-controls`; '' otherwise.\n\t\t *\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t * @return {string}\n\t\t */\n\t\tviewsRegionId() {\n\t\t\tconst hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX))\n\t\t\treturn hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''\n\t\t},\n\n\t\t/**\n\t\t * The checked view: the option whose route is the current route, else\n\t\t * the first option (a switch always has one checked option).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string|null}\n\t\t */\n\t\tactiveView() {\n\t\t\tconst options = this.viewOptions\n\t\t\tif (options.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst activeView = this.cnPageViews ? this.cnPageViews.activeId : null\n\t\t\tconst viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null\n\t\t\tif (viewMatch) {\n\t\t\t\treturn viewMatch.value\n\t\t\t}\n\t\t\tconst current = this.$route && this.$route.name\n\t\t\tconst match = options.find((o) => o.value === current)\n\t\t\treturn match ? match.value : options[0].value\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch: `content.views.ariaLabel`\n\t\t * through the host translate function, else \"View\".\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @return {string}\n\t\t */\n\t\tviewsLabel() {\n\t\t\tconst label = this.content && this.content.views && this.content.views.ariaLabel\n\t\t\treturn label ? this.cnTranslate(label) : t('nextcloud-vue', 'Views')\n\t\t},\n\n\t\t/**\n\t\t * Whether the plain presentation is on (no coloured background).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisPlain() {\n\t\t\treturn Boolean(this.content && (this.content.plain === true || this.content.ground === true))\n\t\t},\n\n\t\t/**\n\t\t * Whether the greeting sits on the page ground (`content.ground`):\n\t\t * the plain look without the card padding, so the date line and the\n\t\t * heading align with the page edge as the board draws them. Off by\n\t\t * default.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground\n\t\t * @return {boolean}\n\t\t */\n\t\tisGround() {\n\t\t\treturn Boolean(this.content && this.content.ground === true)\n\t\t},\n\n\t\t/**\n\t\t * The name the greeting uses: the first word of the display name, or\n\t\t * the whole display name for `greeting: \"full\"`. '' when unknown.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingName() {\n\t\t\tlet user\n\t\t\ttry {\n\t\t\t\tuser = getCurrentUser()\n\t\t\t} catch {\n\t\t\t\tuser = null\n\t\t\t}\n\t\t\tconst displayName = (user && typeof user.displayName === 'string') ? user.displayName.trim() : ''\n\t\t\tif (displayName === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.content.greeting === 'full' ? displayName : displayName.split(/\\s+/)[0]\n\t\t},\n\n\t\t/**\n\t\t * The greeting for the time of day, with the user's name when known,\n\t\t * or '' when the greeting is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tgreetingText() {\n\t\t\tconst greeting = this.content && this.content.greeting\n\t\t\tif (greeting !== true && greeting !== 'full' && greeting !== 'first') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst hour = this.reference.getHours()\n\t\t\tconst name = this.greetingName\n\t\t\tif (hour < 12) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good morning, {name}', { name }) : t('nextcloud-vue', 'Good morning')\n\t\t\t}\n\t\t\tif (hour < 18) {\n\t\t\t\treturn name ? t('nextcloud-vue', 'Good afternoon, {name}', { name }) : t('nextcloud-vue', 'Good afternoon')\n\t\t\t}\n\t\t\treturn name ? t('nextcloud-vue', 'Good evening, {name}', { name }) : t('nextcloud-vue', 'Good evening')\n\t\t},\n\n\t\t/**\n\t\t * The heading: the greeting when it is on, else the title.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\theadingText() {\n\t\t\treturn this.greetingText || this.title\n\t\t},\n\n\t\t/**\n\t\t * Today's date written out (\"Monday 5 October 2026\"), or '' when\n\t\t * `showDate` is off.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateLine() {\n\t\t\tif (!this.content || this.content.showDate !== true) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }\n\t\t\tlet locale\n\t\t\ttry {\n\t\t\t\tlocale = getCanonicalLocale()\n\t\t\t} catch {\n\t\t\t\tlocale = undefined\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale || undefined, options).format(this.reference)\n\t\t\t} catch {\n\t\t\t\treturn new Intl.DateTimeFormat(undefined, options).format(this.reference)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style of the date line: the heading's colour, or the muted\n\t\t * text colour on a plain header.\n\t\t *\n\t\t * @return {object}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tdateStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.isPlain && !(this.content && this.content.textColor) ? 'var(--color-text-maxcontrast)' : this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** Whether a non-empty subtitle is set. */\n\t\thasSubtitle() {\n\t\t\treturn this.subtitle !== ''\n\t\t},\n\n\t\t/** Resolved background image URL (file id wins over URL). */\n\t\tbackgroundImageUrl() {\n\t\t\tconst fileId = this.content && this.content.backgroundImageFileId\n\t\t\tif (typeof fileId === 'number' && Number.isFinite(fileId) && fileId > 0) {\n\t\t\t\treturn `/index.php/core/preview?fileId=${fileId}&x=1024&y=512&a=1`\n\t\t\t}\n\t\t\tconst url = this.content && this.content.backgroundImageUrl\n\t\t\t// Accept http(s) plus uploaded/same-origin sources: data: and blob:\n\t\t\t// (from the form's upload) and root-relative paths (NC files/preview).\n\t\t\t// External http(s) images may still be blocked by the instance CSP —\n\t\t\t// uploading is the reliable, same-origin path. resolveImageUrl() adds\n\t\t\t// the webroot + /index.php to `/apps/...` resource paths so they load\n\t\t\t// on index.php-routed instances (other shapes pass through).\n\t\t\tif (typeof url === 'string' && /^(https?:\\/\\/|data:|blob:|\\/)/i.test(url)) {\n\t\t\t\treturn resolveImageUrl(url)\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/** Whether a usable, non-errored background image is present. */\n\t\thasBackgroundImage() {\n\t\t\treturn this.backgroundImageUrl !== '' && this.imageFailed === false\n\t\t},\n\n\t\t/**\n\t\t * Resolved background colour (default theme primary).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tbackgroundColor() {\n\t\t\tconst value = this.content && this.content.backgroundColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.isPlain) {\n\t\t\t\treturn 'transparent'\n\t\t\t}\n\t\t\treturn 'var(--color-primary, #0070c0)'\n\t\t},\n\n\t\t/** Resolved overlay mode (`tint` when an image is present, else `none`). */\n\t\toverlayMode() {\n\t\t\tconst declared = this.content && this.content.overlayMode\n\t\t\tif (typeof declared === 'string' && ALLOWED_OVERLAY_MODES.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.hasBackgroundImage ? 'tint' : 'none'\n\t\t},\n\n\t\t/** Whether an overlay should render. */\n\t\thasOverlay() {\n\t\t\treturn this.overlayMode !== 'none'\n\t\t},\n\n\t\t/** Resolved overlay colour. */\n\t\toverlayColor() {\n\t\t\tconst value = this.content && this.content.overlayColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst bg = this.content && this.content.backgroundColor\n\t\t\treturn (typeof bg === 'string' && bg !== '') ? bg : '#000000'\n\t\t},\n\n\t\t/** Resolved overlay opacity, clamped to 0..1 (default 0.4). */\n\t\toverlayOpacity() {\n\t\t\tconst raw = this.content && this.content.overlayOpacity\n\t\t\tconst value = typeof raw === 'number' ? raw : Number.parseFloat(raw)\n\t\t\tif (Number.isFinite(value) === false) {\n\t\t\t\treturn 0.4\n\t\t\t}\n\t\t\tif (value < 0) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tif (value > 1) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/** Resolved height preset key (default `medium`). */\n\t\theight() {\n\t\t\tconst declared = this.content && this.content.height\n\t\t\tif (typeof declared === 'string' && ALLOWED_HEIGHTS.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'medium'\n\t\t},\n\n\t\t/** The pixel height for the active preset. */\n\t\theightPixels() {\n\t\t\treturn HEIGHT_PIXELS[this.height]\n\t\t},\n\n\t\t/**\n\t\t * Resolved text alignment (default `center`).\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextAlign() {\n\t\t\tconst declared = this.content && this.content.textAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_TEXT_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn this.isPlain ? 'left' : 'center'\n\t\t},\n\n\t\t/** Resolved vertical alignment (default `middle`). */\n\t\tverticalAlign() {\n\t\t\tconst declared = this.content && this.content.verticalAlign\n\t\t\tif (typeof declared === 'string' && ALLOWED_VERTICAL_ALIGN.includes(declared)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn 'middle'\n\t\t},\n\n\t\t/**\n\t\t * Resolved text colour with an auto-contrast fallback.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\ttextColor() {\n\t\t\tconst value = this.content && this.content.textColor\n\t\t\tif (typeof value === 'string' && value !== '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\treturn '#ffffff'\n\t\t\t}\n\t\t\tif (this.isPlain && !(this.content && this.content.backgroundColor)) {\n\t\t\t\treturn 'var(--color-main-text)'\n\t\t\t}\n\t\t\treturn this.isLightColor(this.backgroundColor) ? '#000000' : '#ffffff'\n\t\t},\n\n\t\t/** Inline style for the banner wrapper. */\n\t\twrapperStyle() {\n\t\t\tconst style = {\n\t\t\t\tposition: 'relative',\n\t\t\t\twidth: '100%',\n\t\t\t\t// Fill the dashboard grid cell so the banner resizes with the\n\t\t\t\t// widget. A fixed pixel height ignored the cell, which made the\n\t\t\t\t// banner un-resizable and made it overflow/scroll inside a\n\t\t\t\t// smaller cell. The grid controls the size now.\n\t\t\t\theight: '100%',\n\t\t\t\toverflow: 'hidden',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t}\n\t\t\tif (this.hasBackgroundImage) {\n\t\t\t\tstyle['background-image'] = `url(\"${this.backgroundImageUrl}\")`\n\t\t\t\tstyle['background-size'] = 'cover'\n\t\t\t\tstyle['background-position'] = 'center center'\n\t\t\t\tstyle['background-repeat'] = 'no-repeat'\n\t\t\t}\n\t\t\treturn style\n\t\t},\n\n\t\t/** Inline style for the overlay layer. */\n\t\toverlayStyle() {\n\t\t\tif (this.overlayMode === 'gradient-bottom') {\n\t\t\t\treturn {\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\tinset: 0,\n\t\t\t\t\t'background-image': `linear-gradient(to bottom, transparent 50%, ${this.overlayColor} 100%)`,\n\t\t\t\t\t'pointer-events': 'none',\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tposition: 'absolute',\n\t\t\t\tinset: 0,\n\t\t\t\t'background-color': this.overlayColor,\n\t\t\t\topacity: String(this.overlayOpacity),\n\t\t\t\t'pointer-events': 'none',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for the content flex container.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-greeting-header\n\t\t */\n\t\tcontentStyle() {\n\t\t\treturn {\n\t\t\t\tposition: 'relative',\n\t\t\t\t'z-index': 1,\n\t\t\t\t// Beside a view switch the content shares the row instead of\n\t\t\t\t// taking the whole width (see .cn-header-widget--with-views).\n\t\t\t\twidth: this.viewOptions.length > 0 ? 'auto' : '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'flex-direction': 'column',\n\t\t\t\t'align-items': this.flexAlignFromTextAlign,\n\t\t\t\t'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],\n\t\t\t\t// Plain drops the coloured background, not the card padding:\n\t\t\t\t// at 0 the greeting sat flush against the card's left edge.\n\t\t\t\t// Ground has no card, so it has no padding either.\n\t\t\t\tpadding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),\n\t\t\t\t'box-sizing': 'border-box',\n\t\t\t\tgap: this.isGround ? '6px' : '8px',\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t}\n\t\t},\n\n\t\t/** Flex cross-axis alignment derived from the text alignment. */\n\t\tflexAlignFromTextAlign() {\n\t\t\tif (this.textAlign === 'left') {\n\t\t\t\treturn 'flex-start'\n\t\t\t}\n\t\t\tif (this.textAlign === 'right') {\n\t\t\t\treturn 'flex-end'\n\t\t\t}\n\t\t\treturn 'center'\n\t\t},\n\n\t\t/** Inline style applying the resolved text colour. */\n\t\ttextStyle() {\n\t\t\treturn {\n\t\t\t\tcolor: this.textColor,\n\t\t\t\tmargin: 0,\n\t\t\t}\n\t\t},\n\n\t\t/** The validated CTA object, or null when incomplete. */\n\t\tcta() {\n\t\t\tconst raw = this.content && this.content.cta\n\t\t\tif (raw === null || typeof raw !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = typeof raw.label === 'string' ? raw.label.trim() : ''\n\t\t\tconst url = typeof raw.url === 'string' ? raw.url.trim() : ''\n\t\t\tif (label === '' || url === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet style = raw.style\n\t\t\tif (typeof style !== 'string' || ALLOWED_CTA_STYLES.includes(style) === false) {\n\t\t\t\tstyle = 'primary'\n\t\t\t}\n\t\t\treturn { label, url, style }\n\t\t},\n\n\t\t/** Whether a complete CTA is present. */\n\t\thasCta() {\n\t\t\treturn this.cta !== null\n\t\t},\n\n\t\t/** The CTA label, translated. */\n\t\tctaLabel() {\n\t\t\treturn this.hasCta && this.cta.label ? this.effectiveTranslate(this.cta.label) : ''\n\t\t},\n\n\t\t/** The CTA URL. */\n\t\tctaUrl() {\n\t\t\treturn this.hasCta ? this.cta.url : ''\n\t\t},\n\n\t\t/** Whether the CTA URL is an external http(s) link. */\n\t\tctaIsExternal() {\n\t\t\treturn this.hasCta && /^https?:\\/\\//i.test(this.ctaUrl)\n\t\t},\n\n\t\t/** The CTA anchor target (`_blank` for external links). */\n\t\tctaTarget() {\n\t\t\treturn this.ctaIsExternal ? '_blank' : null\n\t\t},\n\n\t\t/** The CTA anchor rel for external links. */\n\t\tctaRel() {\n\t\t\treturn this.ctaIsExternal ? 'noopener noreferrer' : null\n\t\t},\n\n\t\t/** The accessible CTA label, annotating external links. */\n\t\tctaAriaLabel() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.ctaIsExternal) {\n\t\t\t\treturn `${this.ctaLabel} (${t('nextcloud-vue', 'opens in new tab')})`\n\t\t\t}\n\t\t\treturn this.ctaLabel\n\t\t},\n\n\t\t/** The CTA class list (base + style modifier). */\n\t\tctaClasses() {\n\t\t\tif (this.hasCta === false) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t'cn-header-widget__cta',\n\t\t\t\t`cn-header-widget__cta--${this.cta.style}`,\n\t\t\t]\n\t\t},\n\t},\n\n\twatch: {\n\t\tbackgroundImageUrl() {\n\t\t\t// Re-arm the probe so a previously failing URL can recover when\n\t\t\t// the user edits the placement.\n\t\t\tthis.imageFailed = false\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Follow the chosen view: push its route (with the option's `params`\n\t\t * when declared). Nothing happens for the route already shown.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views\n\t\t * @param {string} route The chosen option's route name.\n\t\t * @return {void}\n\t\t */\n\t\tonViewChange(route) {\n\t\t\tif (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {\n\t\t\t\tif (this.cnPageViews && typeof this.cnPageViews.select === 'function') {\n\t\t\t\t\tthis.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.$router || !route || route === (this.$route && this.$route.name)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}\n\t\t\tconst target = { name: route }\n\t\t\tif (declared.params && typeof declared.params === 'object') {\n\t\t\t\ttarget.params = declared.params\n\t\t\t}\n\t\t\tthis.$router.push(target)\n\t\t},\n\n\t\t/**\n\t\t * Mark the background image as broken so the renderer falls back to\n\t\t * the solid colour.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonImageError() {\n\t\t\tthis.imageFailed = true\n\t\t},\n\n\t\t/**\n\t\t * Quick perceived-luminance check on a CSS hex colour. Returns true\n\t\t * for \"light\" (white text would be illegible). Non-hex colours\n\t\t * collapse to false (assume dark) — the safer default.\n\t\t *\n\t\t * @param {string} color the CSS colour value.\n\t\t * @return {boolean} true when the colour is perceived as light.\n\t\t */\n\t\tisLightColor(color) {\n\t\t\tif (typeof color !== 'string') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst match = color.trim().match(/^#([0-9a-f]{6})$/i)\n\t\t\tif (!match) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst hex = match[1]\n\t\t\tconst r = Number.parseInt(hex.slice(0, 2), 16)\n\t\t\tconst g = Number.parseInt(hex.slice(2, 4), 16)\n\t\t\tconst b = Number.parseInt(hex.slice(4, 6), 16)\n\t\t\tconst luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255\n\t\t\treturn luma > 0.6\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-header-widget {\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 120px;\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n.cn-header-widget--plain {\n\tmin-height: 0;\n\tborder-radius: 0;\n}\n\n/* With a view switch the heading and the switch share one row, the switch\n at the right and bottom-aligned with the heading; on a narrow card the\n switch wraps under it. */\n.cn-header-widget--with-views {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tjustify-content: space-between;\n\tgap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));\n}\n\n.cn-header-widget--with-views .cn-header-widget__content {\n\tflex: 1 1 320px;\n\twidth: auto;\n}\n\n.cn-header-widget__views {\n\tposition: relative;\n\tz-index: 1;\n\tflex: 0 0 auto;\n\tpadding: 16px;\n}\n\n/* On the page ground (`content.ground`): no card, so no inset around the\n view switch, and the heading at the board's 32px. */\n.cn-header-widget--ground .cn-header-widget__views {\n\tpadding: 0;\n}\n\n.cn-header-widget--ground .cn-header-widget__title {\n\tfont-size: var(--cn-header-ground-title-size, 32px);\n\tletter-spacing: -0.01em;\n}\n\n.cn-header-widget__date {\n\tfont-size: 14px;\n\tline-height: 1.4;\n\tmargin: 0;\n}\n\n.cn-header-widget__overlay {\n\tposition: absolute;\n\tinset: 0;\n}\n\n.cn-header-widget__content {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-header-widget__title {\n\tfont-size: 28px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tmargin: 0;\n}\n\n.cn-header-widget__subtitle {\n\tfont-size: 16px;\n\tline-height: 1.4;\n\tmargin: 0;\n\topacity: 0.95;\n}\n\n.cn-header-widget__cta {\n\tdisplay: inline-block;\n\tmargin-top: 8px;\n\tpadding: 10px 18px;\n\tborder-radius: var(--border-radius, 4px);\n\tfont-size: 14px;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n\ttransition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.cn-header-widget__cta:hover {\n\ttransform: translateY(-1px);\n\tbox-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\n\n.cn-header-widget__cta--primary {\n\tbackground-color: var(--color-primary, #0070c0);\n\tcolor: var(--color-primary-text, #ffffff);\n\tborder: 1px solid var(--color-primary, #0070c0);\n}\n\n.cn-header-widget__cta--secondary {\n\tbackground-color: var(--color-background-hover, rgba(255, 255, 255, 0.85));\n\tcolor: var(--color-main-text, #000000);\n\tborder: 1px solid var(--color-border, #cccccc);\n}\n\n.cn-header-widget__cta--ghost {\n\tbackground-color: transparent;\n\tcolor: inherit;\n\tborder: 1px solid currentColor;\n}\n\n.cn-header-widget__probe {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\topacity: 0;\n\tpointer-events: none;\n\tinset-inline-start: -9999px;\n\ttop: -9999px;\n}\n\n@media print {\n\t.cn-header-widget {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createElementVNode","_createVNode"],"mappings":";;;;;AA6C4B,EAAA,KAAA,EAAA,yBAAA;;;;;;;uBAlCzBA;IAAG,KAAK,EAAAC,cAAA,CAAA,CAAC,kBAAW,EAAA,EAAA,yBAAA,EAAA,QAAA,CAAA,OAAA,8BAAA,QAAA,CAAA,QAAA,EAAA,8BAAA,EAAA,QAAA,CAAA,WAAA,CAAA,MAAA,GAAA,CAAA,EAAA,CAAA,CAAA;AAAA,IAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;KAEnB,QAAA,CAAA,UAAA;sBADFD,kBAImB,CAAA,KAAA,EAAA;;AAFjB,UAAA,KAAI,EAAC,2BAAS;AACd,UAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;AACnB,UAAA,aAAU,EAAC,MAAM;;;IACjBE,kBAuBI,CAAA,KAAA,EAAA;AAvBD,MAAA,KAAK,EAAC,2BAA0B;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;OAEjC,QAAA,CAAA,QAAA;0CAKC,CAAA,GAAA,EAAA;;AAJH,YAAA,KAAK,EAAC,wBAAwB;AAC7B,YAAA,KAAK,iBAAE,QAAQ,CAAA,SAAA,CAAA;AAChB,YAAA,aAAG,EAAA,uBAAA;6BACA,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;OAET,QAAA,CAAA,QAAA;wBAAHF,kBAEK,CAAA,IAAA,EAAA;;AAFF,YAAA,KAAA,EAAA,yBAAA;AAAA,YAAA,KAAA,iBAAA,QAAA,CAAA,SAAA,CAAA;6BACF,QAAA,CAAA,WAAA,CAAA,EAAA,CAAA,mBAAA;;OAEQ,QAAK,CAAA,WAAA;0CAEV,CAAA,GAAA,EAAA;;AAFU,YAAA,KAAA,EAAA,4BAAA;AAAA,YAAA,KAAA,iBAAA,QAAA,CAAA,SAAA,CAAA;6BACV,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;OAGL,QAAM,CAAA,MAAA;0CAOT,CAAA,GAAA,EAAA;;AANF,YAAA,IAAI,EAAE,QAAM,CAAA,MAAA;AACZ,YAAA,KAAC,iBAAA,QAAA,CAAA,UAAA,CAAA;AACD,YAAA,MAAE,EAAA,QAAA,CAAA,SAAA;AACF,YAAA,GAAI,EAAC,QAAM,CAAA,MAAA;wBACD,EAAE,QAAY,CAAA,YAAA;6BACtB,QAAS,CAAA,QAAA,CAAA,EAAA,EAAA,2BAAA,UAAA,CAAA;;;AAOH,IAAA,CAAA,QAAA,CAAA,WAAW,CAAC,MAAG,GAAA,CAAA;yCAQpB,iBAAA,EAAA;UAPJG,WAMuB,CAAA,6BAAA,EAAA;AALtB,YAAA,OAAO,EAAE,QAAW,CAAA,WAAA;AACpB,YAAA,UAAE,EAAA,QAAA,CAAA,UAAA;AACF,YAAA,SAAA,EAAA,QAAA,CAAA,UAAA;AACA,YAAA,QAAQ,EAAE,QAAY,CAAA,aAAA;AACvB,YAAA,aAAW,EAAC,wBAAiB;AAC5B,YAAA,qBAAiB,EAAE,QAAG,CAAA,YAAA;;;;KAGlB,QAAe,CAAA,kBAAA;sBADrBH;;AAEA,UAAA,KAAG,EAAA,yBAAA;;;;;;;;;;;;"}
|
|
@@ -2,6 +2,7 @@ import { getCurrentUser } from '@nextcloud/auth';
|
|
|
2
2
|
import { translate } from '@nextcloud/l10n';
|
|
3
3
|
import { NcLoadingIcon, NcEmptyContent, NcActions, NcActionButton, NcActionCaption, NcActionCheckbox, NcButton } from '@nextcloud/vue';
|
|
4
4
|
import { getCurrentInstance, ref, markRaw, inject } from 'vue';
|
|
5
|
+
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue';
|
|
5
6
|
import Close from 'vue-material-design-icons/Close.vue';
|
|
6
7
|
import Cog from 'vue-material-design-icons/Cog.vue';
|
|
7
8
|
import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue';
|
|
@@ -285,6 +286,7 @@ var script = {
|
|
|
285
286
|
NcActionCaption,
|
|
286
287
|
NcActionCheckbox,
|
|
287
288
|
NcButton,
|
|
289
|
+
AlertCircleOutline,
|
|
288
290
|
Close,
|
|
289
291
|
Cog,
|
|
290
292
|
DatabaseSearch,
|
|
@@ -1265,6 +1267,17 @@ var script = {
|
|
|
1265
1267
|
default: false,
|
|
1266
1268
|
},
|
|
1267
1269
|
|
|
1270
|
+
/**
|
|
1271
|
+
* Keep the "Showing X of Y" counter visible beside the inline search field,
|
|
1272
|
+
* after any `#after-search` controls (only relevant with `inlineSearch`,
|
|
1273
|
+
* which otherwise takes the counter's place). Fed from the manifest as
|
|
1274
|
+
* `pages[].config.showCountWithSearch`.
|
|
1275
|
+
*/
|
|
1276
|
+
showCountWithSearch: {
|
|
1277
|
+
type: Boolean,
|
|
1278
|
+
default: false,
|
|
1279
|
+
},
|
|
1280
|
+
|
|
1268
1281
|
/** Placeholder for the inline search field (manifest `config.searchPlaceholder`) */
|
|
1269
1282
|
searchPlaceholder: {
|
|
1270
1283
|
type: String,
|
|
@@ -2165,11 +2178,23 @@ var script = {
|
|
|
2165
2178
|
return this.isSelfFetch && !!this.list
|
|
2166
2179
|
},
|
|
2167
2180
|
|
|
2181
|
+
/**
|
|
2182
|
+
* Whether the latest self-fetch failed. The store keeps the previous
|
|
2183
|
+
* rows and pagination on a failed fetch, so while this holds the page
|
|
2184
|
+
* shows its error state instead of results for an earlier query.
|
|
2185
|
+
*/
|
|
2186
|
+
selfFetchFailed() {
|
|
2187
|
+
return this.isSelfFetchMode && !!this.list.error?.value
|
|
2188
|
+
},
|
|
2189
|
+
|
|
2168
2190
|
/** Rows: store collection in self-fetch mode, else the `objects` prop. */
|
|
2169
2191
|
effectiveObjects() {
|
|
2170
2192
|
if (this.isNamedSource) {
|
|
2171
2193
|
return this.namedRows
|
|
2172
2194
|
}
|
|
2195
|
+
if (this.selfFetchFailed) {
|
|
2196
|
+
return []
|
|
2197
|
+
}
|
|
2173
2198
|
return this.isSelfFetchMode ? (this.list.objects.value || []) : this.objects
|
|
2174
2199
|
},
|
|
2175
2200
|
|
|
@@ -2573,7 +2598,9 @@ var script = {
|
|
|
2573
2598
|
s: this.exportSchemaSlug,
|
|
2574
2599
|
f: this.filter || null,
|
|
2575
2600
|
e: this.countEntries,
|
|
2576
|
-
|
|
2601
|
+
// The store's total, not the error override in effectivePagination,
|
|
2602
|
+
// so a failed fetch and its recovery do not refetch the counts.
|
|
2603
|
+
t: Number((this.isSelfFetchMode ? this.list.pagination.value : this.effectivePagination)?.total ?? 0),
|
|
2577
2604
|
})
|
|
2578
2605
|
},
|
|
2579
2606
|
|
|
@@ -2979,6 +3006,9 @@ var script = {
|
|
|
2979
3006
|
|
|
2980
3007
|
/** Pagination: store pagination in self-fetch mode, else the `pagination` prop. */
|
|
2981
3008
|
effectivePagination() {
|
|
3009
|
+
if (this.selfFetchFailed) {
|
|
3010
|
+
return { ...this.list.pagination.value, total: 0, page: 1, pages: 1 }
|
|
3011
|
+
}
|
|
2982
3012
|
return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
|
|
2983
3013
|
},
|
|
2984
3014
|
|