@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
@@ -217,6 +217,7 @@
217
217
  <component
218
218
  :is="resolveFieldRender(field).tag"
219
219
  v-if="resolveFieldRender(field)"
220
+ v-cn-select-aria="selectAria(field)"
220
221
  v-bind="fieldProps(field)"
221
222
  v-on="resolveFieldRender(field).listeners">
222
223
  <!-- NcCheckboxRadioSwitch puts its label in the slot -->
@@ -398,6 +399,7 @@ import CnStepper from '../CnStepper/CnStepper.vue'
398
399
  import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
399
400
  import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
400
401
  import { normalizeLook } from '../../composables/useLook.js'
402
+ import { cnSelectAria } from '../../directives/cnSelectAria.js'
401
403
  import { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'
402
404
  import { computeAssignments, resolveFieldDefaults } from '../../utils/formAssign.js'
403
405
  import { validateFieldValue } from '../../utils/formValidation.js'
@@ -441,6 +443,8 @@ function resolveParams(url, params) {
441
443
  export default {
442
444
  name: 'CnFormPage',
443
445
 
446
+ directives: { cnSelectAria },
447
+
444
448
  components: {
445
449
  CnFormErrorSummary,
446
450
  CnFormField,
@@ -1308,6 +1312,35 @@ export default {
1308
1312
  return props
1309
1313
  },
1310
1314
 
1315
+ /**
1316
+ * The accessibility attributes of an enum field's NcSelect. NcSelect
1317
+ * forwards no attributes to its input, so these go through
1318
+ * `v-cn-select-aria` onto the combobox input instead of `fieldProps`.
1319
+ * The error element exists for an enum in both looks (NcSelect has no
1320
+ * native error text), so an invalid value lists it before the hint.
1321
+ *
1322
+ * @param {object} field The formField shape.
1323
+ * @return {object|null} The attributes, or null for any other control.
1324
+ */
1325
+ selectAria(field) {
1326
+ const render = this.resolveFieldRender(field)
1327
+ if (!render || render.kind !== 'enum') {
1328
+ return null
1329
+ }
1330
+ const ids = []
1331
+ if (this.fieldErrors[field.key]) {
1332
+ ids.push(this.errorIdFor(field))
1333
+ }
1334
+ if (field.help) {
1335
+ ids.push(this.helpIdFor(field))
1336
+ }
1337
+ return {
1338
+ 'aria-invalid': this.fieldErrors[field.key] ? 'true' : null,
1339
+ 'aria-describedby': ids.length > 0 ? ids.join(' ') : null,
1340
+ 'aria-required': this.showsBoardHead(field) && this.isFieldRequired(field) ? 'true' : null,
1341
+ }
1342
+ },
1343
+
1311
1344
  /**
1312
1345
  * Cancel (board look footer): go to `cancelRoute` when set, and tell the
1313
1346
  * host.
@@ -82,7 +82,7 @@
82
82
  :activeFilterChips="activeFilterChips"
83
83
  :showBuildiqButton="!buildiqInHeader"
84
84
  :bulkNoun="boardBulkNoun"
85
- :bulkHint="bulkHint"
85
+ :bulkHint="bulkHint ? cnTranslate(bulkHint) : bulkHint"
86
86
  :pagination="effectivePagination"
87
87
  :objectCount="effectiveObjects.length"
88
88
  :selectable="selectable"
@@ -848,7 +848,7 @@
848
848
  <CnPagination
849
849
  v-if="effectivePagination && (effectivePagination.pages > 1 || (isBoardLook && effectivePagination.total > 0))"
850
850
  :variant="isBoardLook ? 'board' : ''"
851
- :footerNote="footerNote"
851
+ :footerNote="footerNote ? cnTranslate(footerNote) : footerNote"
852
852
  :currentPage="effectivePagination.page || 1"
853
853
  :totalPages="effectivePagination.pages || 1"
854
854
  :totalItems="effectivePagination.total || 0"
@@ -46,7 +46,7 @@
46
46
  :name="entry.tab.icon"
47
47
  :size="16"
48
48
  class="cn-quick-filter-bar__icon" />
49
- <span class="cn-quick-filter-bar__label">{{ entry.tab.label }}</span>
49
+ <span class="cn-quick-filter-bar__label">{{ tl(entry.tab.label) }}</span>
50
50
  <span
51
51
  v-if="countOf(entry) !== null"
52
52
  class="cn-quick-filter-bar__count"
@@ -102,7 +102,7 @@
102
102
  :name="entry.tab.icon"
103
103
  :size="16"
104
104
  class="cn-quick-filter-bar__icon" />
105
- <span class="cn-quick-filter-bar__label">{{ entry.tab.label }}</span>
105
+ <span class="cn-quick-filter-bar__label">{{ tl(entry.tab.label) }}</span>
106
106
  <span
107
107
  v-if="countOf(entry) !== null"
108
108
  class="cn-quick-filter-bar__count"
@@ -152,6 +152,11 @@ export default {
152
152
  inject: {
153
153
  /** The look CnAppRoot provides; `board` draws the tabs as pressed chips. */
154
154
  cnLook: { default: 'nextcloud' },
155
+ /**
156
+ * The host's label lookup (CnAppRoot provides it); identity without one, so a
157
+ * label that is not a key renders as written.
158
+ */
159
+ cnTranslate: { default: () => (key) => key },
155
160
  },
156
161
 
157
162
  // NO `model: { prop, event }` OPTION.
@@ -336,7 +341,7 @@ export default {
336
341
  overflowLabel() {
337
342
  const active = this.activeOverflowEntries
338
343
  if (active.length === 1) {
339
- return active[0].tab.label
344
+ return this.tl(active[0].tab.label)
340
345
  }
341
346
  if (active.length > 1) {
342
347
  return t('nextcloud-vue', '{count} filters', { count: active.length })
@@ -370,7 +375,7 @@ export default {
370
375
  .map((tab, index) => {
371
376
  const count = this.countOf({ tab, index })
372
377
  return {
373
- label: count === null ? tab.label : `${tab.label} (${count})`,
378
+ label: count === null ? this.tl(tab.label) : `${this.tl(tab.label)} (${count})`,
374
379
  icon: tab.icon,
375
380
  index,
376
381
  _empty: this.isEmptyFilter(tab),
@@ -394,6 +399,17 @@ export default {
394
399
  },
395
400
 
396
401
  methods: {
402
+ /**
403
+ * A manifest string through the host's label lookup (`cnTranslate`), so a key
404
+ * such as "Mine" reads in the user's language.
405
+ *
406
+ * @param {string} text The text as written in the manifest.
407
+ * @return {string} The translated text.
408
+ */
409
+ tl(text) {
410
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
411
+ },
412
+
397
413
  t,
398
414
 
399
415
  /**
@@ -39,7 +39,7 @@
39
39
  </slot>
40
40
  </span>
41
41
  <h4 class="cn-kpi-card__title" :title="title || undefined">
42
- {{ title || t('nextcloud-vue', 'Objects') }}
42
+ {{ title ? tl(title) : t('nextcloud-vue', 'Objects') }}
43
43
  </h4>
44
44
  </div>
45
45
 
@@ -54,7 +54,7 @@
54
54
  <!-- @binding {string} formatted The default localized count string. -->
55
55
  <slot name="value" :count="count" :formatted="formattedCount">{{ formattedCount }}</slot>
56
56
  </span>
57
- <span class="cn-kpi-card__label cn-stats-block__count-label">{{ countLabel }}</span>
57
+ <span class="cn-kpi-card__label cn-stats-block__count-label">{{ countLabel ? tl(countLabel) : countLabel }}</span>
58
58
  </div>
59
59
  <div v-else-if="loading" class="cn-stats-block__loading">
60
60
  <NcLoadingIcon :size="16" />
@@ -164,6 +164,11 @@ export default {
164
164
  * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile
165
165
  */
166
166
  cnLook: { default: 'nextcloud' },
167
+ /**
168
+ * The host's label lookup (CnAppRoot provides it); identity without one, so a
169
+ * label that is not a key renders as written.
170
+ */
171
+ cnTranslate: { default: () => (key) => key },
167
172
  },
168
173
 
169
174
  props: {
@@ -453,6 +458,17 @@ export default {
453
458
  },
454
459
 
455
460
  methods: {
461
+ /**
462
+ * A manifest string through the host's label lookup (`cnTranslate`), so a key
463
+ * such as "Mine" reads in the user's language.
464
+ *
465
+ * @param {string} text The text as written in the manifest.
466
+ * @return {string} The translated text.
467
+ */
468
+ tl(text) {
469
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
470
+ },
471
+
456
472
  formatBreakdownLabel(key) {
457
473
  return key.charAt(0).toUpperCase() + key.slice(1) + ':'
458
474
  },
@@ -88,7 +88,7 @@
88
88
  }
89
89
 
90
90
  /* An invalid field: 2px error border on the control, 4px edge on the group. */
91
- .cn-look-board .cn-form-field--invalid input,
91
+ .cn-look-board .cn-form-field--invalid input:not([type='checkbox']):not([type='radio']),
92
92
  .cn-look-board .cn-form-field--invalid textarea,
93
93
  .cn-look-board .cn-form-field--invalid .select {
94
94
  border: var(--cn-field-error-border) solid var(--color-error);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * `v-cn-select-aria`: puts `aria-invalid`, `aria-describedby` and
6
+ * `aria-required` on the search input of an NcSelect.
7
+ *
8
+ * NcSelect forwards no attributes to its input: an attribute bound on it lands
9
+ * on the wrapping `.v-select` div, where a screen reader never reads it. The
10
+ * input is the combobox, so the attributes belong there. This directive sets
11
+ * them on that input after every render, and removes the ones that go away
12
+ * (a field that becomes valid loses `aria-invalid`).
13
+ *
14
+ * Usage: `<NcSelect v-cn-select-aria="{ 'aria-invalid': 'true', 'aria-describedby': 'err-id' }" />`
15
+ * A `null` or `undefined` value does nothing, so a caller can bind it
16
+ * unconditionally.
17
+ *
18
+ * @spec openspec/changes/screens-form-parity/tasks.md
19
+ */
20
+
21
+ const ARIA_ATTRIBUTES = ['aria-invalid', 'aria-describedby', 'aria-required']
22
+
23
+ /**
24
+ * Apply the bound attributes to the combobox input inside the element.
25
+ *
26
+ * @param {HTMLElement} el The element the directive sits on (the select root).
27
+ * @param {{value: object|null|undefined}} binding The directive binding.
28
+ * @return {void}
29
+ */
30
+ function apply(el, binding) {
31
+ const attributes = binding.value
32
+ if (!attributes || typeof el.querySelector !== 'function') {
33
+ return
34
+ }
35
+ const input = el.querySelector('input.vs__search') || el.querySelector('input')
36
+ if (!input) {
37
+ return
38
+ }
39
+ for (const name of ARIA_ATTRIBUTES) {
40
+ const value = attributes[name]
41
+ if (value === undefined || value === null || value === false) {
42
+ input.removeAttribute(name)
43
+ } else {
44
+ input.setAttribute(name, String(value))
45
+ }
46
+ }
47
+ }
48
+
49
+ export const cnSelectAria = {
50
+ mounted: apply,
51
+ updated: apply,
52
+ }
53
+
54
+ export default cnSelectAria
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.72.0",
6
+ "version": "2.73.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -326,6 +326,11 @@
326
326
  "alt": {
327
327
  "type": "string",
328
328
  "description": "i18n key / alternative text for the logo. Used only when no `name` is set."
329
+ },
330
+ "placement": {
331
+ "type": "string",
332
+ "enum": ["header", "nav"],
333
+ "description": "Where the brand block is drawn (screens-brand-block-top-bar). Under `look: \"board\"` the default is `header`: a 237px block at the start of Nextcloud's own header (#header), before the app menu, drawn by CnAppRoot; an app without CnAppNav has it too. Nextcloud offers no extension point there, so when `#header` is absent the block falls back to the navigation. `nav` keeps the block at the top of the app navigation. Ignored without the board look, where the navigation draws the block."
329
334
  }
330
335
  }
331
336
  },
@@ -59,11 +59,26 @@ export function configureCnFetch(config = {}) {
59
59
  }
60
60
  transport = {
61
61
  host,
62
- baseUrl: typeof config.baseUrl === 'string' ? config.baseUrl.replace(/\/+$/, '') : '',
62
+ baseUrl: typeof config.baseUrl === 'string' ? stripTrailingSlashes(config.baseUrl) : '',
63
63
  credential: config.credential || null,
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Drop trailing `/` characters in one linear pass (a `/\/+$/` pattern
69
+ * backtracks quadratically on a long run of slashes).
70
+ *
71
+ * @param {string} value The string to trim.
72
+ * @return {string} The string without trailing slashes.
73
+ */
74
+ export function stripTrailingSlashes(value) {
75
+ let end = value.length
76
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
77
+ end--
78
+ }
79
+ return value.slice(0, end)
80
+ }
81
+
67
82
  /**
68
83
  * The bearer credential, resolved.
69
84
  *
@@ -0,0 +1,37 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Reads a declared brand (`nav.brand`, or the `brand` prop of CnAppNav and
6
+ * CnAppRoot) into the one shape the brand block and the brand bar draw.
7
+ *
8
+ * @spec openspec/changes/screens-brand-block-top-bar/tasks.md
9
+ */
10
+
11
+ /**
12
+ * Resolve a declared brand.
13
+ *
14
+ * @param {object|null|undefined} declared The declared brand.
15
+ * @param {(text: string) => string} translate Label lookup for name, caption and alt.
16
+ * @return {{logo: string, emblem: (string|boolean), name: string, caption: string, alt: string, placement: string}|null}
17
+ * The brand, or null when nothing is declared that can be drawn.
18
+ */
19
+ export function resolveBrand(declared, translate) {
20
+ if (!declared || typeof declared !== 'object') {
21
+ return null
22
+ }
23
+ const text = (value) => (typeof value === 'string' && value !== '' ? translate(value) : '')
24
+ const brand = {
25
+ logo: typeof declared.logo === 'string' ? declared.logo : '',
26
+ // A URL, or `true` for the theme's emblem (--nldesign-emblem-url).
27
+ emblem: declared.emblem === true ? true : (typeof declared.emblem === 'string' ? declared.emblem : ''),
28
+ name: text(declared.name),
29
+ caption: text(declared.caption),
30
+ alt: text(declared.alt),
31
+ // '' means "not declared": the caller decides the default from the look.
32
+ placement: declared.placement === 'nav' || declared.placement === 'header' ? declared.placement : '',
33
+ }
34
+ return (brand.logo || brand.emblem || brand.name || brand.caption) ? brand : null
35
+ }
36
+
37
+ export default resolveBrand