@conduction/nextcloud-vue 2.74.0 → 2.76.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 (126) 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/CnCardGrid/CnCardGrid.vue2.js +6 -0
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
  23. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +8 -5
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
  29. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
  30. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
  36. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
  38. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +144 -6
  40. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +14 -13
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
  44. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
  46. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  47. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
  48. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
  50. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  51. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
  52. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  54. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
  55. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  56. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
  57. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  58. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
  59. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
  61. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
  63. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  64. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
  65. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  66. package/dist/esm/composables/useObjectLock.js +41 -10
  67. package/dist/esm/composables/useObjectLock.js.map +1 -1
  68. package/dist/esm/directives/cnSelectAria.js +55 -0
  69. package/dist/esm/directives/cnSelectAria.js.map +1 -0
  70. package/dist/esm/index.js +1 -0
  71. package/dist/esm/index.js.map +1 -1
  72. package/dist/esm/l10n/en.json.js +7 -1
  73. package/dist/esm/l10n/en.json.js.map +1 -1
  74. package/dist/esm/l10n/nl.json.js +8 -2
  75. package/dist/esm/l10n/nl.json.js.map +1 -1
  76. package/dist/esm/nextcloud-vue.css +154 -2
  77. package/dist/esm/store/useObjectStore.js +37 -9
  78. package/dist/esm/store/useObjectStore.js.map +1 -1
  79. package/dist/esm/utils/cnFetch.js +17 -2
  80. package/dist/esm/utils/cnFetch.js.map +1 -1
  81. package/dist/esm/utils/objectSignals.js +44 -0
  82. package/dist/esm/utils/objectSignals.js.map +1 -0
  83. package/dist/esm/utils/resolveBrand.js +38 -0
  84. package/dist/esm/utils/resolveBrand.js.map +1 -0
  85. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  86. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  87. package/dist/nextcloud-vue.cjs.js +9640 -8649
  88. package/dist/nextcloud-vue.cjs.js.map +2 -2
  89. package/dist/nextcloud-vue.css +154 -2
  90. package/l10n/en.json +7 -1
  91. package/l10n/nl.json +8 -2
  92. package/package.json +1 -1
  93. package/src/components/CnAppNav/CnAppNav.vue +21 -16
  94. package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
  95. package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
  96. package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
  97. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  98. package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
  99. package/src/components/CnCredentials/CnCredentials.vue +17 -16
  100. package/src/components/CnDetailPage/CnDetailPage.vue +8 -5
  101. package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
  102. package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
  103. package/src/components/CnFormPage/CnFormPage.vue +33 -0
  104. package/src/components/CnIndexPage/CnIndexPage.md +15 -0
  105. package/src/components/CnIndexPage/CnIndexPage.vue +157 -18
  106. package/src/components/CnIndexPage/selfModeActions.js +89 -13
  107. package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
  108. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  109. package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
  110. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
  111. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
  112. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
  113. package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
  114. package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
  115. package/src/composables/useObjectLock.js +41 -10
  116. package/src/css/form-field.css +1 -1
  117. package/src/css/look-board-index.css +35 -1
  118. package/src/directives/cnSelectAria.js +54 -0
  119. package/src/index.js +1 -0
  120. package/src/schemas/app-manifest-v2.schema.json +6 -1
  121. package/src/store/useObjectStore.js +37 -9
  122. package/src/utils/cnFetch.js +16 -1
  123. package/src/utils/index.js +1 -0
  124. package/src/utils/objectSignals.js +41 -0
  125. package/src/utils/resolveBrand.js +37 -0
  126. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -343,7 +343,10 @@
343
343
  }
344
344
 
345
345
  .cn-look-board .cn-index-page__main.cn-index-page__main--table {
346
- flex: 0 1 auto;
346
+ /* The body is a row: the card takes the width left beside a sidebar (the
347
+ board's table card spans the content), and keeps the height of its rows. */
348
+ flex: 1 1 auto;
349
+ align-self: flex-start;
347
350
  border: 1px solid var(--color-border);
348
351
  border-radius: var(--cn-board-card-radius, 12px);
349
352
  background: var(--color-main-background);
@@ -533,6 +536,37 @@
533
536
  background: var(--color-primary-element-light);
534
537
  }
535
538
 
539
+ /* The status accent outranks the board's border, also on hover and selection. */
540
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent {
541
+ border-inline-start-width: 4px;
542
+ padding-inline-start: 13px;
543
+ }
544
+
545
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-success,
546
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-success:hover {
547
+ border-inline-start-color: var(--color-element-success);
548
+ }
549
+
550
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-warning,
551
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-warning:hover {
552
+ border-inline-start-color: var(--color-element-warning);
553
+ }
554
+
555
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-error,
556
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-error:hover {
557
+ border-inline-start-color: var(--color-element-error);
558
+ }
559
+
560
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-info,
561
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-info:hover {
562
+ border-inline-start-color: var(--color-element-info);
563
+ }
564
+
565
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-primary,
566
+ .cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-primary:hover {
567
+ border-inline-start-color: var(--color-primary-element);
568
+ }
569
+
536
570
  .cn-look-board .cn-object-card--board .cn-object-card__content {
537
571
  display: flex;
538
572
  flex: 1 1 auto;
@@ -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
package/src/index.js CHANGED
@@ -542,6 +542,7 @@ export { cnFetch, cnFetchJson, CnHttpError, configureCnFetch } from './utils/cnF
542
542
  // CnLockIndicator, CnObjectCard and CnDataTable, and available to apps that
543
543
  // need the same answer without rendering a padlock.
544
544
  export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './utils/objectLock.js'
545
+ export { dispatchObjectsChanged } from './utils/objectSignals.js'
545
546
  export { columnsFromSchema, fieldsFromSchema, filtersFromSchema, findRepeatingWrite, formatValue, journeyItemTargets, validateValue } from './utils/index.js'
546
547
  export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData, usesArrayValues, valueArrayFor, valueRecordsFor } from './utils/index.js'
547
548
  // The OpenRegister schema API contract — shared so Buildiq and OpenRegister cannot
@@ -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
  },
@@ -240,9 +240,10 @@ const baseActions = {
240
240
  * @param {object} [slugs] Optional slug hints for live-updates transport
241
241
  * @param {string|null} [slugs.registerSlug] Canonical register slug (e.g. 'zaken')
242
242
  * @param {string|null} [slugs.schemaSlug] Canonical schema slug (e.g. 'meldingen')
243
+ * @param {string} [slugs.collectionUrl] Endpoint `fetchCollection` lists from instead of `/register/schema`; every other call keeps the pair URL
243
244
  */
244
245
  registerObjectType(slug, schemaId, registerId, slugs = {}) {
245
- const { registerSlug = null, schemaSlug = null } = slugs
246
+ const { registerSlug = null, schemaSlug = null, collectionUrl = null } = slugs
246
247
  // Replace entire objects so Vue 2 reactivity detects the change
247
248
  // (Vue 2 cannot track new properties added to existing reactive objects)
248
249
  this.objectTypeRegistry = {
@@ -252,6 +253,7 @@ const baseActions = {
252
253
  register: registerId,
253
254
  registerSlug,
254
255
  schemaSlug,
256
+ ...(collectionUrl ? { collectionUrl } : {}),
255
257
  },
256
258
  }
257
259
  this.collections = { ...this.collections, [slug]: [] }
@@ -496,6 +498,31 @@ const baseActions = {
496
498
  return url
497
499
  },
498
500
 
501
+ /**
502
+ * Build the list URL for a type registered with a `collectionUrl`. `_order`
503
+ * goes in the bracket form (`_order[key]=dir`) every PHP endpoint parses.
504
+ *
505
+ * @param {object} config The type config
506
+ * @param {object} [params] Query parameters
507
+ * @return {string} Full URL including query string
508
+ */
509
+ _buildCollectionUrl(config, params = {}) {
510
+ const { _order: order, ...rest } = params
511
+ const merged = { ...rest }
512
+ if (order && typeof order === 'object') {
513
+ for (const [key, dir] of Object.entries(order)) {
514
+ merged[`_order[${key}]`] = dir
515
+ }
516
+ }
517
+ const lang = this._resolveLanguage()
518
+ if (lang) {
519
+ merged._lang = lang
520
+ }
521
+ const url = prefixUrl(config.collectionUrl)
522
+ const query = buildQueryString(merged)
523
+ return query && url.includes('?') ? url + '&' + query.slice(1) : url + query
524
+ },
525
+
499
526
  /**
500
527
  * Fetch for a type, retrying ONCE with its `schemaFallback` when the
501
528
  * schema path segment answers 404. A type without a fallback (every type
@@ -685,14 +712,15 @@ const baseActions = {
685
712
  }
686
713
  }
687
714
 
688
- const response = await this._fetchWithSchemaFallback(
689
- type,
690
- (schema) => this._buildUrlWithParams(type, fetchParams, null, schema),
691
- {
692
- method: 'GET',
693
- headers: this._buildHeaders(),
694
- },
695
- )
715
+ const config = this._getTypeConfig(type)
716
+ const init = { method: 'GET', headers: this._buildHeaders() }
717
+ const response = config.collectionUrl
718
+ ? await trackedFetch(this._buildCollectionUrl(config, fetchParams), init)
719
+ : await this._fetchWithSchemaFallback(
720
+ type,
721
+ (schema) => this._buildUrlWithParams(type, fetchParams, null, schema),
722
+ init,
723
+ )
696
724
 
697
725
  if (!response.ok) {
698
726
  const failure = await parseResponseError(response, type)
@@ -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
  *
@@ -19,3 +19,4 @@ export { validateFieldValue } from './formValidation.js'
19
19
  export { computeObjectDiff } from './computeObjectDiff.js'
20
20
  export { foldAuditTrailEntries } from './auditTrailDiff.js'
21
21
  export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './objectLock.js'
22
+ export { dispatchObjectsChanged } from './objectSignals.js'
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Signal that objects changed outside the surface showing them, such as files
6
+ * uploaded or an object saved from an app's own modal. CnIndexPage listens and
7
+ * refreshes its list when the register and schema match.
8
+ *
9
+ * @module utils/objectSignals
10
+ */
11
+
12
+ export const OBJECTS_CHANGED_EVENT = 'cn:objects-changed'
13
+
14
+ /**
15
+ * Announce that objects of a register and schema changed.
16
+ *
17
+ * Pass the same register and schema identifiers (slug or id) the listing page
18
+ * uses. Leaving one out matches every page on that part.
19
+ *
20
+ * @param {object} [payload] What changed.
21
+ * @param {string|number} [payload.register] The register slug or id.
22
+ * @param {string|number} [payload.schema] The schema slug or id.
23
+ * @param {string} [payload.id] The changed object's id, when there is one.
24
+ * @return {boolean} True when the event was dispatched.
25
+ */
26
+ export function dispatchObjectsChanged({ register, schema, id } = {}) {
27
+ if (typeof window === 'undefined') {
28
+ return false
29
+ }
30
+ const detail = {
31
+ register: register === undefined || register === null || register === '' ? null : String(register),
32
+ schema: schema === undefined || schema === null || schema === '' ? null : String(schema),
33
+ id: id ? String(id) : null,
34
+ }
35
+ try {
36
+ window.dispatchEvent(new CustomEvent(OBJECTS_CHANGED_EVENT, { detail }))
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
@@ -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