@conduction/nextcloud-vue 2.74.0 → 2.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +19 -14
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +2 -2
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +77 -26
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +22 -14
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +23 -4
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js +9 -0
  13. package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js.map +1 -0
  14. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js +107 -0
  15. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -0
  16. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js +79 -0
  17. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -0
  18. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
  19. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
  20. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
  22. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  23. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
  24. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
  26. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
  28. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
  33. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  34. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
  35. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  36. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
  37. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  38. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
  39. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  40. package/dist/esm/directives/cnSelectAria.js +55 -0
  41. package/dist/esm/directives/cnSelectAria.js.map +1 -0
  42. package/dist/esm/nextcloud-vue.css +66 -1
  43. package/dist/esm/utils/cnFetch.js +17 -2
  44. package/dist/esm/utils/cnFetch.js.map +1 -1
  45. package/dist/esm/utils/resolveBrand.js +38 -0
  46. package/dist/esm/utils/resolveBrand.js.map +1 -0
  47. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  48. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  49. package/dist/nextcloud-vue.cjs.js +8775 -8327
  50. package/dist/nextcloud-vue.cjs.js.map +2 -2
  51. package/dist/nextcloud-vue.css +66 -1
  52. package/package.json +1 -1
  53. package/src/components/CnAppNav/CnAppNav.vue +21 -16
  54. package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
  55. package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
  56. package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
  57. package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
  58. package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
  59. package/src/components/CnFormPage/CnFormPage.vue +33 -0
  60. package/src/components/CnIndexPage/CnIndexPage.vue +2 -2
  61. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
  62. package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
  63. package/src/css/form-field.css +1 -1
  64. package/src/directives/cnSelectAria.js +54 -0
  65. package/src/schemas/app-manifest-v2.schema.json +6 -1
  66. package/src/utils/cnFetch.js +16 -1
  67. package/src/utils/resolveBrand.js +37 -0
  68. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -0,0 +1,79 @@
1
+ import { openBlock, createBlock, Teleport, createElementVNode, createElementBlock, createCommentVNode, toDisplayString } from 'vue';
2
+
3
+ const _hoisted_1 = {
4
+ class: "cn-brand-bar",
5
+ "data-testid": "cn-brand-bar"
6
+ };
7
+ const _hoisted_2 = {
8
+ class: "cn-brand-bar__brand",
9
+ "data-testid": "cn-brand-bar-brand"
10
+ };
11
+ const _hoisted_3 = ["src", "alt"];
12
+ const _hoisted_4 = {
13
+ key: 1,
14
+ class: "cn-brand-bar__emblem cn-brand-bar__emblem--theme",
15
+ "data-testid": "cn-brand-bar-emblem",
16
+ "aria-hidden": "true"
17
+ };
18
+ const _hoisted_5 = ["src", "alt"];
19
+ const _hoisted_6 = {
20
+ key: 3,
21
+ class: "cn-brand-bar__text"
22
+ };
23
+ const _hoisted_7 = {
24
+ key: 0,
25
+ class: "cn-brand-bar__caption"
26
+ };
27
+ const _hoisted_8 = {
28
+ key: 1,
29
+ class: "cn-brand-bar__name"
30
+ };
31
+
32
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
33
+ return ($data.host)
34
+ ? (openBlock(), createBlock(Teleport, {
35
+ key: 0,
36
+ to: $data.host
37
+ }, [
38
+ createElementVNode("div", _hoisted_1, [
39
+ createElementVNode("div", _hoisted_2, [
40
+ (typeof $props.brand.emblem === 'string' && $props.brand.emblem !== '')
41
+ ? (openBlock(), createElementBlock("img", {
42
+ key: 0,
43
+ class: "cn-brand-bar__emblem",
44
+ "data-testid": "cn-brand-bar-emblem",
45
+ src: $props.brand.emblem,
46
+ alt: $props.brand.name ? '' : $props.brand.alt
47
+ }, null, 8 /* PROPS */, _hoisted_3))
48
+ : ($props.brand.emblem === true)
49
+ ? (openBlock(), createElementBlock("span", _hoisted_4))
50
+ : ($props.brand.logo)
51
+ ? (openBlock(), createElementBlock("img", {
52
+ key: 2,
53
+ class: "cn-brand-bar__emblem",
54
+ src: $props.brand.logo,
55
+ alt: $props.brand.name ? '' : $props.brand.alt
56
+ }, null, 8 /* PROPS */, _hoisted_5))
57
+ : createCommentVNode("v-if", true),
58
+ ($props.brand.name || $props.brand.caption)
59
+ ? (openBlock(), createElementBlock("span", _hoisted_6, [
60
+ ($props.brand.caption)
61
+ ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString($props.brand.caption), 1 /* TEXT */))
62
+ : createCommentVNode("v-if", true),
63
+ ($props.brand.name)
64
+ ? (openBlock(), createElementBlock("strong", _hoisted_8, toDisplayString($props.brand.name), 1 /* TEXT */))
65
+ : createCommentVNode("v-if", true)
66
+ ]))
67
+ : createCommentVNode("v-if", true)
68
+ ]),
69
+ _cache[0] || (_cache[0] = createElementVNode("span", {
70
+ class: "cn-brand-bar__divider",
71
+ "aria-hidden": "true"
72
+ }, null, -1 /* CACHED */))
73
+ ])
74
+ ], 8 /* PROPS */, ["to"]))
75
+ : createCommentVNode("v-if", true)
76
+ }
77
+
78
+ export { render };
79
+ //# sourceMappingURL=CnBrandBar.vue3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnBrandBar.vue3.js","sources":["../../../../src/components/CnBrandBar/CnBrandBar.vue?vue&type=template&id=47a7a5af&lang.js"],"sourcesContent":["<!--\n CnBrandBar — the brand block in Nextcloud's own header (screens-brand-block-top-bar).\n\n Draws the 237px brand block (emblem, organisation, app name, and a 1px\n divider at its end) at the START of `#header`, before the app menu, under the\n board look. CnAppRoot mounts it when a brand is declared and `#header` exists.\n\n `#header` is Nextcloud's and offers no extension point for apps, so this is\n the one place the library reaches into it, and it does so defensively:\n - it adds ONE node of its own (a host element) as the first child of the\n header, and teleports the block into it; it never moves, edits or removes\n a node Nextcloud drew;\n - it re-inserts its own host when Nextcloud re-renders the header (a\n MutationObserver on the header and its parent), never a second one;\n - with no `#header` it renders nothing and emits `unavailable`, so\n CnAppRoot lets the navigation draw the block instead.\n\n Sizes come from the board: `dossiq/DqKop`.\n-->\n<template>\n\t<Teleport v-if=\"host\" :to=\"host\">\n\t\t<div class=\"cn-brand-bar\" data-testid=\"cn-brand-bar\">\n\t\t\t<div class=\"cn-brand-bar__brand\" data-testid=\"cn-brand-bar-brand\">\n\t\t\t\t<!-- Decorative beside a name: the name already says whose app this is. -->\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"typeof brand.emblem === 'string' && brand.emblem !== ''\"\n\t\t\t\t\tclass=\"cn-brand-bar__emblem\"\n\t\t\t\t\tdata-testid=\"cn-brand-bar-emblem\"\n\t\t\t\t\t:src=\"brand.emblem\"\n\t\t\t\t\t:alt=\"brand.name ? '' : brand.alt\">\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"brand.emblem === true\"\n\t\t\t\t\tclass=\"cn-brand-bar__emblem cn-brand-bar__emblem--theme\"\n\t\t\t\t\tdata-testid=\"cn-brand-bar-emblem\"\n\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t<img\n\t\t\t\t\tv-else-if=\"brand.logo\"\n\t\t\t\t\tclass=\"cn-brand-bar__emblem\"\n\t\t\t\t\t:src=\"brand.logo\"\n\t\t\t\t\t:alt=\"brand.name ? '' : brand.alt\">\n\t\t\t\t<span v-if=\"brand.name || brand.caption\" class=\"cn-brand-bar__text\">\n\t\t\t\t\t<span v-if=\"brand.caption\" class=\"cn-brand-bar__caption\">{{ brand.caption }}</span>\n\t\t\t\t\t<strong v-if=\"brand.name\" class=\"cn-brand-bar__name\">{{ brand.name }}</strong>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<span class=\"cn-brand-bar__divider\" aria-hidden=\"true\" />\n\t\t</div>\n\t</Teleport>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\n\nconst HOST_CLASS = 'cn-brand-bar-host'\n\n/**\n * The brand block in Nextcloud's header.\n *\n * @event unavailable Emitted when there is no `#header` to put the block in (or it disappears), so the host lets the navigation draw it.\n *\n * @spec openspec/changes/screens-brand-block-top-bar/specs/layout-components/spec.md#requirement-the-board-look-draws-the-brand-block-in-the-nextcloud-header\n */\nexport default {\n\tname: 'CnBrandBar',\n\n\tprops: {\n\t\t/**\n\t\t * The resolved brand: `{ logo, emblem, name, caption, alt }`. `emblem`\n\t\t * is a URL, or `true` for the theme's `--nldesign-emblem-url`.\n\t\t *\n\t\t * @type {{logo?: string, emblem?: (string|boolean), name?: string, caption?: string, alt?: string}}\n\t\t */\n\t\tbrand: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\temits: ['unavailable'],\n\n\tdata() {\n\t\treturn { host: null, observer: null }\n\t},\n\n\tmounted() {\n\t\tthis.attach()\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.detach()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Insert the host as the first child of `#header` and watch the header.\n\t\t * Emits `unavailable` when there is no header.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tattach() {\n\t\t\tconst header = document.getElementById('header')\n\t\t\tif (!header) {\n\t\t\t\t/**\n\t\t\t\t * @event unavailable Emitted when there is no `#header` to put the block in.\n\t\t\t\t */\n\t\t\t\tthis.$emit('unavailable')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.host = markRaw(document.createElement('div'))\n\t\t\tthis.host.className = HOST_CLASS\n\t\t\theader.insertBefore(this.host, header.firstChild)\n\t\t\tif (typeof MutationObserver === 'function') {\n\t\t\t\tthis.observer = new MutationObserver(() => this.reattach())\n\t\t\t\tthis.observer.observe(header, { childList: true })\n\t\t\t\tif (header.parentNode) {\n\t\t\t\t\tthis.observer.observe(header.parentNode, { childList: true })\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Put the same host back when Nextcloud re-rendered the header and took\n\t\t * it out. Never creates a second host; when the header is gone for good\n\t\t * it hands the block back to the navigation.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treattach() {\n\t\t\tif (!this.host) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst header = document.getElementById('header')\n\t\t\tif (!header) {\n\t\t\t\tthis.$emit('unavailable')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.host.parentNode !== header) {\n\t\t\t\theader.insertBefore(this.host, header.firstChild)\n\t\t\t\tthis.observer?.observe(header, { childList: true })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Stop watching and remove only the host this component added.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdetach() {\n\t\t\tthis.observer?.disconnect()\n\t\t\tthis.observer = null\n\t\t\tthis.host?.remove()\n\t\t\tthis.host = null\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* The host is one flex item at the start of Nextcloud's header. */\n.cn-brand-bar-host {\n\tflex: none;\n\tdisplay: flex;\n\talign-items: center;\n\theight: 100%;\n\tmargin-inline-end: 12px;\n}\n\n.cn-brand-bar {\n\tbox-sizing: border-box;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\twidth: var(--cn-board-brand-width, 237px);\n}\n\n.cn-brand-bar__brand {\n\tflex: 1 1 0;\n\tbox-sizing: border-box;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-width: 0;\n\tmin-height: 48px;\n\tmax-height: 100%;\n\tpadding: 0 6px;\n\tborder-radius: 8px;\n\tcolor: var(--color-background-plain-text, var(--color-main-text));\n}\n\n.cn-brand-bar__emblem {\n\tflex: none;\n\theight: var(--cn-nav-emblem-size, 34px);\n\twidth: auto;\n\tobject-fit: contain;\n}\n\n.cn-brand-bar__emblem--theme {\n\tdisplay: inline-block;\n\twidth: var(--cn-nav-emblem-size, 34px);\n\tbackground: var(--nldesign-emblem-url) center / contain no-repeat;\n}\n\n.cn-brand-bar__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tline-height: 1.15;\n\tmin-width: 0;\n}\n\n.cn-brand-bar__caption {\n\tfont-size: 13px;\n\twhite-space: nowrap;\n\topacity: 0.8;\n}\n\n.cn-brand-bar__name {\n\tfont-size: 18px;\n\tfont-weight: 700;\n\twhite-space: nowrap;\n}\n\n.cn-brand-bar__divider {\n\tflex: none;\n\twidth: 1px;\n\theight: 28px;\n\tbackground: currentColor;\n\topacity: 0.25;\n\tcolor: var(--color-background-plain-text, var(--color-main-text));\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_createElementBlock"],"mappings":";;;AAwCO,EAAA,KAAK,EAAC,cAAc;AAAC,EAAA,aAAW,EAAC,cAAc;;;AAC9C,EAAA,KAAK,EAAC,qBAAqB;AAAC,EAAA,aAAW,EAAC,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;UAFnD,KAAK,CAAA,IAAA;oBAAjBA;;AAAmB,QAAA,EAAC,EAAI,KAAG,CAAA,IAAA;;AAC5B,QAAAC,kBAAA,QAAA;AACC,UAAAA,kBAAA,QAAA;AAGG,YAAA,CAAA,OAAA,MAAA,CAAA,KAAA,CAAA,MAAA,KAAA,QAAA,IAAA,MAAA,CAAA,KAAA,CAAA,MAAA,KAAA,EAAA;8BADJC;;AAEC,kBAAA,KAAK,EAAC,sBAAsB;AAC5B,kBAAA,aAAE,EAAA,qBAAA;kBACD,GAAG,EAAE,MAAA,CAAA,KAAA,CAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -86,7 +86,10 @@ var script = {
86
86
  return
87
87
  }
88
88
  const all = Object.values(ENVIRONMENT_TITLE_PREFIX);
89
- const bare = document.title.replace(new RegExp(`^(?:${all.map((p) => p.replace(/[[\]]/g, '\\$&')).join('|')}) `), '');
89
+ // Plain string comparison: no pattern is built from the prefixes, so
90
+ // there is nothing to escape.
91
+ const owned = all.find((p) => document.title.startsWith(`${p} `));
92
+ const bare = owned ? document.title.slice(owned.length + 1) : document.title;
90
93
  const wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare;
91
94
  if (document.title !== wanted) {
92
95
  document.title = wanted;
@@ -1 +1 @@
1
- {"version":3,"file":"CnEnvironmentBanner.vue2.js","sources":["../../../../src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tv-if=\"marked\"\n\t\tclass=\"cn-environment-banner\"\n\t\t:class=\"`cn-environment-banner--${marked}`\"\n\t\trole=\"note\"\n\t\t:aria-label=\"label\"\n\t\tdata-testid=\"cn-environment-banner\">\n\t\t<span class=\"cn-environment-banner__label\">{{ label }}</span>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { ENVIRONMENT_TITLE_PREFIX, MARKED_ENVIRONMENTS, normaliseEnvironment } from '../../utils/environment.js'\n\n/**\n * CnEnvironmentBanner — names a non-production environment on every app screen\n * so acceptance is not mistaken for production.\n *\n * Shows \"Development environment\", \"Test environment\" or \"Acceptance\n * environment\" in words, with a colour per environment, as a `role=\"note\"` with\n * an accessible name. It has no close action and cannot be hidden by a user\n * setting. While it shows, the document title is prefixed `[DEV]`, `[TEST]` or\n * `[ACC]` (and put back when it goes). Production, an unknown value and no\n * value render nothing and leave the title alone.\n *\n * `CnAppRoot` and `CnAdminSettingsShell` place it themselves, from their\n * `environment` prop or the active organisation's `environment` field. An app\n * that does not mount them can place it:\n *\n * ```vue\n * <CnEnvironmentBanner environment=\"acceptance\" />\n * ```\n */\nexport default {\n\tname: 'CnEnvironmentBanner',\n\n\tprops: {\n\t\t/** The environment: `development`, `test` or `acceptance` shows the banner; anything else shows nothing. */\n\t\tenvironment: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} The environment to mark, or '' when nothing is shown. */\n\t\tmarked() {\n\t\t\tconst name = normaliseEnvironment(this.environment)\n\t\t\treturn MARKED_ENVIRONMENTS.includes(name) ? name : ''\n\t\t},\n\n\t\t/** @return {string} The name of the environment in words. */\n\t\tlabel() {\n\t\t\treturn {\n\t\t\t\tdevelopment: t('nextcloud-vue', 'Development environment'),\n\t\t\t\ttest: t('nextcloud-vue', 'Test environment'),\n\t\t\t\tacceptance: t('nextcloud-vue', 'Acceptance environment'),\n\t\t\t}[this.marked] || ''\n\t\t},\n\t},\n\n\twatch: {\n\t\tmarked: {\n\t\t\timmediate: true,\n\t\t\thandler(next) {\n\t\t\t\tthis.applyTitlePrefix(next)\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// A router that sets the title after we did must not lose the prefix.\n\t\tconst titleElement = typeof document !== 'undefined' ? document.querySelector('title') : null\n\t\tif (titleElement && typeof MutationObserver !== 'undefined') {\n\t\t\tthis.titleObserver = new MutationObserver(() => this.applyTitlePrefix(this.marked))\n\t\t\tthis.titleObserver.observe(titleElement, { childList: true, characterData: true, subtree: true })\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.titleObserver) {\n\t\t\tthis.titleObserver.disconnect()\n\t\t}\n\t\tthis.applyTitlePrefix('')\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Put the environment's prefix at the start of the tab title, or take ours\n\t\t * off when nothing is marked.\n\t\t *\n\t\t * @param {string} environment The marked environment, or ''.\n\t\t * @return {void}\n\t\t */\n\t\tapplyTitlePrefix(environment) {\n\t\t\tif (typeof document === 'undefined') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst all = Object.values(ENVIRONMENT_TITLE_PREFIX)\n\t\t\tconst bare = document.title.replace(new RegExp(`^(?:${all.map((p) => p.replace(/[[\\]]/g, '\\\\$&')).join('|')}) `), '')\n\t\t\tconst wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare\n\t\t\tif (document.title !== wanted) {\n\t\t\t\tdocument.title = wanted\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-environment-banner {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: 24px;\n\tpadding: 2px 12px;\n\tborder-inline-start: 6px solid var(--cn-environment-accent);\n\tborder-block-end: 2px solid var(--cn-environment-accent);\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n}\n\n.cn-environment-banner--development {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n}\n\n.cn-environment-banner--test {\n\t--cn-environment-accent: var(--color-element-warning, var(--color-warning));\n}\n\n.cn-environment-banner--acceptance {\n\t--cn-environment-accent: var(--color-element-error, var(--color-error));\n}\n</style>\n"],"names":["t"],"mappings":";;;AAoBA;AACC;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAED,aAAe;CACd,IAAI,EAAE,qBAAqB;;AAE3B,CAAA,KAAK,EAAE;AACN;EACA,WAAW,EAAE;GACZ,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;EACD;;AAED,CAAA,QAAQ,EAAE;AACT;AACA,EAAA,MAAM,GAAG;GACR,MAAM,OAAO,oBAAoB,CAAC,IAAI,CAAC,WAAW,EAAA;GAClD,OAAO,mBAAmB,CAAC,QAAQ,CAAC,IAAI,CAAE,GAAE,IAAK,GAAE,EAAC;GACpD;;;AAGD,EAAA,KAAK,GAAG;AACP,GAAA,OAAO;IACN,WAAW,EAAEA,SAAC,CAAC,eAAe,EAAE,yBAAyB,CAAC;AAC1D,IAAA,IAAI,EAAEA,SAAC,CAAC,eAAe,EAAE,kBAAkB,CAAC;IAC5C,UAAU,EAAEA,SAAC,CAAC,eAAe,EAAE,wBAAwB,CAAC;AACzD,IAAC,CAAC,IAAI,CAAC,MAAM,CAAE,IAAG,EAAC;GACnB;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,MAAM,EAAE;AACP,GAAA,SAAS,EAAE,IAAI;GACf,OAAO,CAAC,IAAI,EAAE;IACb,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAA;IAC1B;GACD;EACD;;AAED,CAAA,OAAO,GAAG;;AAET,EAAA,MAAM,YAAW,GAAI,OAAO,QAAO,KAAM,WAAU,GAAI,QAAQ,CAAC,aAAa,CAAC,OAAO,IAAI,KAAG;EAC5F,IAAI,YAAa,IAAG,OAAO,gBAAe,KAAM,WAAW,EAAE;AAC5D,GAAA,IAAI,CAAC,aAAY,GAAI,IAAI,gBAAgB,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAA;AAClF,GAAA,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAA;AACjG,GAAA;EACA;;AAED,CAAA,aAAa,GAAG;EACf,IAAI,IAAI,CAAC,aAAa,EAAE;AACvB,GAAA,IAAI,CAAC,aAAa,CAAC,UAAU,GAAC;AAC/B,GAAA;EACA,IAAI,CAAC,gBAAgB,CAAC,EAAE,EAAA;EACxB;;AAED,CAAA,OAAO,EAAE;AACR;;;AAGC;;;;EAID,gBAAgB,CAAC,WAAW,EAAE;AAC7B,GAAA,IAAI,OAAO,QAAS,KAAI,WAAW,EAAE;IACpC,MAAK;AACN,IAAA;GACA,MAAM,GAAE,GAAI,MAAM,CAAC,MAAM,CAAC,wBAAwB,EAAA;GAClD,MAAM,IAAK,GAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAA;AACpH,GAAA,MAAM,SAAS,WAAU,GAAI,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA,CAAE,EAAE,IAAI,CAAC,CAAA,GAAI,KAAG;GACrF,IAAI,QAAQ,CAAC,KAAM,KAAI,MAAM,EAAE;AAC9B,IAAA,QAAQ,CAAC,KAAM,GAAE,OAAK;AACvB,IAAA;GACA;EACD;AACF;;;;"}
1
+ {"version":3,"file":"CnEnvironmentBanner.vue2.js","sources":["../../../../src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tv-if=\"marked\"\n\t\tclass=\"cn-environment-banner\"\n\t\t:class=\"`cn-environment-banner--${marked}`\"\n\t\trole=\"note\"\n\t\t:aria-label=\"label\"\n\t\tdata-testid=\"cn-environment-banner\">\n\t\t<span class=\"cn-environment-banner__label\">{{ label }}</span>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { ENVIRONMENT_TITLE_PREFIX, MARKED_ENVIRONMENTS, normaliseEnvironment } from '../../utils/environment.js'\n\n/**\n * CnEnvironmentBanner — names a non-production environment on every app screen\n * so acceptance is not mistaken for production.\n *\n * Shows \"Development environment\", \"Test environment\" or \"Acceptance\n * environment\" in words, with a colour per environment, as a `role=\"note\"` with\n * an accessible name. It has no close action and cannot be hidden by a user\n * setting. While it shows, the document title is prefixed `[DEV]`, `[TEST]` or\n * `[ACC]` (and put back when it goes). Production, an unknown value and no\n * value render nothing and leave the title alone.\n *\n * `CnAppRoot` and `CnAdminSettingsShell` place it themselves, from their\n * `environment` prop or the active organisation's `environment` field. An app\n * that does not mount them can place it:\n *\n * ```vue\n * <CnEnvironmentBanner environment=\"acceptance\" />\n * ```\n */\nexport default {\n\tname: 'CnEnvironmentBanner',\n\n\tprops: {\n\t\t/** The environment: `development`, `test` or `acceptance` shows the banner; anything else shows nothing. */\n\t\tenvironment: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} The environment to mark, or '' when nothing is shown. */\n\t\tmarked() {\n\t\t\tconst name = normaliseEnvironment(this.environment)\n\t\t\treturn MARKED_ENVIRONMENTS.includes(name) ? name : ''\n\t\t},\n\n\t\t/** @return {string} The name of the environment in words. */\n\t\tlabel() {\n\t\t\treturn {\n\t\t\t\tdevelopment: t('nextcloud-vue', 'Development environment'),\n\t\t\t\ttest: t('nextcloud-vue', 'Test environment'),\n\t\t\t\tacceptance: t('nextcloud-vue', 'Acceptance environment'),\n\t\t\t}[this.marked] || ''\n\t\t},\n\t},\n\n\twatch: {\n\t\tmarked: {\n\t\t\timmediate: true,\n\t\t\thandler(next) {\n\t\t\t\tthis.applyTitlePrefix(next)\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// A router that sets the title after we did must not lose the prefix.\n\t\tconst titleElement = typeof document !== 'undefined' ? document.querySelector('title') : null\n\t\tif (titleElement && typeof MutationObserver !== 'undefined') {\n\t\t\tthis.titleObserver = new MutationObserver(() => this.applyTitlePrefix(this.marked))\n\t\t\tthis.titleObserver.observe(titleElement, { childList: true, characterData: true, subtree: true })\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.titleObserver) {\n\t\t\tthis.titleObserver.disconnect()\n\t\t}\n\t\tthis.applyTitlePrefix('')\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Put the environment's prefix at the start of the tab title, or take ours\n\t\t * off when nothing is marked.\n\t\t *\n\t\t * @param {string} environment The marked environment, or ''.\n\t\t * @return {void}\n\t\t */\n\t\tapplyTitlePrefix(environment) {\n\t\t\tif (typeof document === 'undefined') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst all = Object.values(ENVIRONMENT_TITLE_PREFIX)\n\t\t\t// Plain string comparison: no pattern is built from the prefixes, so\n\t\t\t// there is nothing to escape.\n\t\t\tconst owned = all.find((p) => document.title.startsWith(`${p} `))\n\t\t\tconst bare = owned ? document.title.slice(owned.length + 1) : document.title\n\t\t\tconst wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare\n\t\t\tif (document.title !== wanted) {\n\t\t\t\tdocument.title = wanted\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-environment-banner {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: 24px;\n\tpadding: 2px 12px;\n\tborder-inline-start: 6px solid var(--cn-environment-accent);\n\tborder-block-end: 2px solid var(--cn-environment-accent);\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n}\n\n.cn-environment-banner--development {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n}\n\n.cn-environment-banner--test {\n\t--cn-environment-accent: var(--color-element-warning, var(--color-warning));\n}\n\n.cn-environment-banner--acceptance {\n\t--cn-environment-accent: var(--color-element-error, var(--color-error));\n}\n</style>\n"],"names":["t"],"mappings":";;;AAoBA;AACC;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAED,aAAe;CACd,IAAI,EAAE,qBAAqB;;AAE3B,CAAA,KAAK,EAAE;AACN;EACA,WAAW,EAAE;GACZ,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;EACD;;AAED,CAAA,QAAQ,EAAE;AACT;AACA,EAAA,MAAM,GAAG;GACR,MAAM,OAAO,oBAAoB,CAAC,IAAI,CAAC,WAAW,EAAA;GAClD,OAAO,mBAAmB,CAAC,QAAQ,CAAC,IAAI,CAAE,GAAE,IAAK,GAAE,EAAC;GACpD;;;AAGD,EAAA,KAAK,GAAG;AACP,GAAA,OAAO;IACN,WAAW,EAAEA,SAAC,CAAC,eAAe,EAAE,yBAAyB,CAAC;AAC1D,IAAA,IAAI,EAAEA,SAAC,CAAC,eAAe,EAAE,kBAAkB,CAAC;IAC5C,UAAU,EAAEA,SAAC,CAAC,eAAe,EAAE,wBAAwB,CAAC;AACzD,IAAC,CAAC,IAAI,CAAC,MAAM,CAAE,IAAG,EAAC;GACnB;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,MAAM,EAAE;AACP,GAAA,SAAS,EAAE,IAAI;GACf,OAAO,CAAC,IAAI,EAAE;IACb,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAA;IAC1B;GACD;EACD;;AAED,CAAA,OAAO,GAAG;;AAET,EAAA,MAAM,YAAW,GAAI,OAAO,QAAO,KAAM,WAAU,GAAI,QAAQ,CAAC,aAAa,CAAC,OAAO,IAAI,KAAG;EAC5F,IAAI,YAAa,IAAG,OAAO,gBAAe,KAAM,WAAW,EAAE;AAC5D,GAAA,IAAI,CAAC,aAAY,GAAI,IAAI,gBAAgB,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAA;AAClF,GAAA,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAA;AACjG,GAAA;EACA;;AAED,CAAA,aAAa,GAAG;EACf,IAAI,IAAI,CAAC,aAAa,EAAE;AACvB,GAAA,IAAI,CAAC,aAAa,CAAC,UAAU,GAAC;AAC/B,GAAA;EACA,IAAI,CAAC,gBAAgB,CAAC,EAAE,EAAA;EACxB;;AAED,CAAA,OAAO,EAAE;AACR;;;AAGC;;;;EAID,gBAAgB,CAAC,WAAW,EAAE;AAC7B,GAAA,IAAI,OAAO,QAAS,KAAI,WAAW,EAAE;IACpC,MAAK;AACN,IAAA;GACA,MAAM,GAAE,GAAI,MAAM,CAAC,MAAM,CAAC,wBAAwB,EAAA;;;GAGlD,MAAM,KAAI,GAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,CAAA,CAAC,CAAC,EAAA;AAChE,GAAA,MAAM,IAAG,GAAI,KAAM,GAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAO,GAAE,CAAC,CAAA,GAAI,QAAQ,CAAC,MAAI;AAC3E,GAAA,MAAM,SAAS,WAAU,GAAI,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA,CAAE,EAAE,IAAI,CAAC,CAAA,GAAI,KAAG;GACrF,IAAI,QAAQ,CAAC,KAAM,KAAI,MAAM,EAAE;AAC9B,IAAA,QAAQ,CAAC,KAAM,GAAE,OAAK;AACvB,IAAA;GACA;EACD;AACF;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnEnvironmentBanner.vue3.js","sources":["../../../../src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue?vue&type=template&id=3c3362b9&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tv-if=\"marked\"\n\t\tclass=\"cn-environment-banner\"\n\t\t:class=\"`cn-environment-banner--${marked}`\"\n\t\trole=\"note\"\n\t\t:aria-label=\"label\"\n\t\tdata-testid=\"cn-environment-banner\">\n\t\t<span class=\"cn-environment-banner__label\">{{ label }}</span>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { ENVIRONMENT_TITLE_PREFIX, MARKED_ENVIRONMENTS, normaliseEnvironment } from '../../utils/environment.js'\n\n/**\n * CnEnvironmentBanner — names a non-production environment on every app screen\n * so acceptance is not mistaken for production.\n *\n * Shows \"Development environment\", \"Test environment\" or \"Acceptance\n * environment\" in words, with a colour per environment, as a `role=\"note\"` with\n * an accessible name. It has no close action and cannot be hidden by a user\n * setting. While it shows, the document title is prefixed `[DEV]`, `[TEST]` or\n * `[ACC]` (and put back when it goes). Production, an unknown value and no\n * value render nothing and leave the title alone.\n *\n * `CnAppRoot` and `CnAdminSettingsShell` place it themselves, from their\n * `environment` prop or the active organisation's `environment` field. An app\n * that does not mount them can place it:\n *\n * ```vue\n * <CnEnvironmentBanner environment=\"acceptance\" />\n * ```\n */\nexport default {\n\tname: 'CnEnvironmentBanner',\n\n\tprops: {\n\t\t/** The environment: `development`, `test` or `acceptance` shows the banner; anything else shows nothing. */\n\t\tenvironment: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} The environment to mark, or '' when nothing is shown. */\n\t\tmarked() {\n\t\t\tconst name = normaliseEnvironment(this.environment)\n\t\t\treturn MARKED_ENVIRONMENTS.includes(name) ? name : ''\n\t\t},\n\n\t\t/** @return {string} The name of the environment in words. */\n\t\tlabel() {\n\t\t\treturn {\n\t\t\t\tdevelopment: t('nextcloud-vue', 'Development environment'),\n\t\t\t\ttest: t('nextcloud-vue', 'Test environment'),\n\t\t\t\tacceptance: t('nextcloud-vue', 'Acceptance environment'),\n\t\t\t}[this.marked] || ''\n\t\t},\n\t},\n\n\twatch: {\n\t\tmarked: {\n\t\t\timmediate: true,\n\t\t\thandler(next) {\n\t\t\t\tthis.applyTitlePrefix(next)\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// A router that sets the title after we did must not lose the prefix.\n\t\tconst titleElement = typeof document !== 'undefined' ? document.querySelector('title') : null\n\t\tif (titleElement && typeof MutationObserver !== 'undefined') {\n\t\t\tthis.titleObserver = new MutationObserver(() => this.applyTitlePrefix(this.marked))\n\t\t\tthis.titleObserver.observe(titleElement, { childList: true, characterData: true, subtree: true })\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.titleObserver) {\n\t\t\tthis.titleObserver.disconnect()\n\t\t}\n\t\tthis.applyTitlePrefix('')\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Put the environment's prefix at the start of the tab title, or take ours\n\t\t * off when nothing is marked.\n\t\t *\n\t\t * @param {string} environment The marked environment, or ''.\n\t\t * @return {void}\n\t\t */\n\t\tapplyTitlePrefix(environment) {\n\t\t\tif (typeof document === 'undefined') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst all = Object.values(ENVIRONMENT_TITLE_PREFIX)\n\t\t\tconst bare = document.title.replace(new RegExp(`^(?:${all.map((p) => p.replace(/[[\\]]/g, '\\\\$&')).join('|')}) `), '')\n\t\t\tconst wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare\n\t\t\tif (document.title !== wanted) {\n\t\t\t\tdocument.title = wanted\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-environment-banner {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: 24px;\n\tpadding: 2px 12px;\n\tborder-inline-start: 6px solid var(--cn-environment-accent);\n\tborder-block-end: 2px solid var(--cn-environment-accent);\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n}\n\n.cn-environment-banner--development {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n}\n\n.cn-environment-banner--test {\n\t--cn-environment-accent: var(--color-element-warning, var(--color-warning));\n}\n\n.cn-environment-banner--acceptance {\n\t--cn-environment-accent: var(--color-element-error, var(--color-error));\n}\n</style>\n"],"names":["_normalizeClass"],"mappings":";;;;;;UAUQ,QAAM,CAAA,MAAA;;;QACZ,KAAK,EAAAA,cAAA,CAAA,CAAC,uBAAuB,EAAA,CAAA,uBAAA,EACK,QAAM,CAAA,MAAA,CAAA,CAAA,CAAA,CAAA;AACxC,QAAA,IAAI,EAAA,MAAA;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnEnvironmentBanner.vue3.js","sources":["../../../../src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue?vue&type=template&id=3c3362b9&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tv-if=\"marked\"\n\t\tclass=\"cn-environment-banner\"\n\t\t:class=\"`cn-environment-banner--${marked}`\"\n\t\trole=\"note\"\n\t\t:aria-label=\"label\"\n\t\tdata-testid=\"cn-environment-banner\">\n\t\t<span class=\"cn-environment-banner__label\">{{ label }}</span>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { ENVIRONMENT_TITLE_PREFIX, MARKED_ENVIRONMENTS, normaliseEnvironment } from '../../utils/environment.js'\n\n/**\n * CnEnvironmentBanner — names a non-production environment on every app screen\n * so acceptance is not mistaken for production.\n *\n * Shows \"Development environment\", \"Test environment\" or \"Acceptance\n * environment\" in words, with a colour per environment, as a `role=\"note\"` with\n * an accessible name. It has no close action and cannot be hidden by a user\n * setting. While it shows, the document title is prefixed `[DEV]`, `[TEST]` or\n * `[ACC]` (and put back when it goes). Production, an unknown value and no\n * value render nothing and leave the title alone.\n *\n * `CnAppRoot` and `CnAdminSettingsShell` place it themselves, from their\n * `environment` prop or the active organisation's `environment` field. An app\n * that does not mount them can place it:\n *\n * ```vue\n * <CnEnvironmentBanner environment=\"acceptance\" />\n * ```\n */\nexport default {\n\tname: 'CnEnvironmentBanner',\n\n\tprops: {\n\t\t/** The environment: `development`, `test` or `acceptance` shows the banner; anything else shows nothing. */\n\t\tenvironment: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} The environment to mark, or '' when nothing is shown. */\n\t\tmarked() {\n\t\t\tconst name = normaliseEnvironment(this.environment)\n\t\t\treturn MARKED_ENVIRONMENTS.includes(name) ? name : ''\n\t\t},\n\n\t\t/** @return {string} The name of the environment in words. */\n\t\tlabel() {\n\t\t\treturn {\n\t\t\t\tdevelopment: t('nextcloud-vue', 'Development environment'),\n\t\t\t\ttest: t('nextcloud-vue', 'Test environment'),\n\t\t\t\tacceptance: t('nextcloud-vue', 'Acceptance environment'),\n\t\t\t}[this.marked] || ''\n\t\t},\n\t},\n\n\twatch: {\n\t\tmarked: {\n\t\t\timmediate: true,\n\t\t\thandler(next) {\n\t\t\t\tthis.applyTitlePrefix(next)\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// A router that sets the title after we did must not lose the prefix.\n\t\tconst titleElement = typeof document !== 'undefined' ? document.querySelector('title') : null\n\t\tif (titleElement && typeof MutationObserver !== 'undefined') {\n\t\t\tthis.titleObserver = new MutationObserver(() => this.applyTitlePrefix(this.marked))\n\t\t\tthis.titleObserver.observe(titleElement, { childList: true, characterData: true, subtree: true })\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.titleObserver) {\n\t\t\tthis.titleObserver.disconnect()\n\t\t}\n\t\tthis.applyTitlePrefix('')\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Put the environment's prefix at the start of the tab title, or take ours\n\t\t * off when nothing is marked.\n\t\t *\n\t\t * @param {string} environment The marked environment, or ''.\n\t\t * @return {void}\n\t\t */\n\t\tapplyTitlePrefix(environment) {\n\t\t\tif (typeof document === 'undefined') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst all = Object.values(ENVIRONMENT_TITLE_PREFIX)\n\t\t\t// Plain string comparison: no pattern is built from the prefixes, so\n\t\t\t// there is nothing to escape.\n\t\t\tconst owned = all.find((p) => document.title.startsWith(`${p} `))\n\t\t\tconst bare = owned ? document.title.slice(owned.length + 1) : document.title\n\t\t\tconst wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare\n\t\t\tif (document.title !== wanted) {\n\t\t\t\tdocument.title = wanted\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-environment-banner {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: 24px;\n\tpadding: 2px 12px;\n\tborder-inline-start: 6px solid var(--cn-environment-accent);\n\tborder-block-end: 2px solid var(--cn-environment-accent);\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n}\n\n.cn-environment-banner--development {\n\t--cn-environment-accent: var(--color-element-info, var(--color-primary-element));\n}\n\n.cn-environment-banner--test {\n\t--cn-environment-accent: var(--color-element-warning, var(--color-warning));\n}\n\n.cn-environment-banner--acceptance {\n\t--cn-environment-accent: var(--color-element-error, var(--color-error));\n}\n</style>\n"],"names":["_normalizeClass"],"mappings":";;;;;;UAUQ,QAAM,CAAA,MAAA;;;QACZ,KAAK,EAAAA,cAAA,CAAA,CAAC,uBAAuB,EAAA,CAAA,uBAAA,EACK,QAAM,CAAA,MAAA,CAAA,CAAA,CAAA,CAAA;AACxC,QAAA,IAAI,EAAA,MAAA;;;;;;;;;;;"}
@@ -22,6 +22,7 @@ import { splitHeldFiles, uploadHeldFiles, heldFromFailures } from '../../composa
22
22
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js';
23
23
  import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js';
24
24
  import { useWriteFeedback } from '../../composables/useWriteFeedback.js';
25
+ import { cnSelectAria } from '../../directives/cnSelectAria.js';
25
26
  import { dialogBoardMixin } from '../../mixins/dialogBoard.js';
26
27
  import { useObjectStore } from '../../store/useObjectStore.js';
27
28
  import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js';
@@ -233,6 +234,8 @@ const SEMANTIC_RESOLVE_ENDPOINT = '/apps/openregister/api/schemas/resolve-by-imp
233
234
  var script = {
234
235
  name: 'CnFormDialog',
235
236
 
237
+ directives: { cnSelectAria },
238
+
236
239
  components: {
237
240
  CnFileField: __sfc_main,
238
241
  CnDurationField: __sfc_main$1,
@@ -2842,6 +2845,10 @@ var script = {
2842
2845
  return
2843
2846
  }
2844
2847
  const [head, ...rest] = key.split('.');
2848
+ const unsafe = ['__proto__', 'constructor', 'prototype'];
2849
+ if (unsafe.includes(head) || rest.some((k) => unsafe.includes(k))) {
2850
+ return
2851
+ }
2845
2852
  const clone = JSON.parse(JSON.stringify(this.formData[head] ?? {}));
2846
2853
  let cur = clone;
2847
2854
  rest.slice(0, -1).forEach((k) => {