@conduction/nextcloud-vue 2.93.0 → 2.95.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 (162) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +46 -28
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  17. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
  18. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +91 -36
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +115 -72
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +32 -3
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
  34. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
  36. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
  38. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
  40. package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
  45. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
  47. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
  48. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
  49. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
  55. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
  56. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
  57. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
  58. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
  59. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
  60. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
  61. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
  62. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
  63. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
  64. package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
  65. package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
  66. package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +51 -4
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  73. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +230 -25
  74. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
  76. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  77. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  78. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
  79. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  80. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
  81. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  82. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  83. package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
  84. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  86. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  87. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
  88. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
  90. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  91. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  92. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  93. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
  94. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  95. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  96. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  97. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  98. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +20 -0
  99. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  100. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
  101. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  102. package/dist/esm/composables/indexSources.js +9 -0
  103. package/dist/esm/composables/indexSources.js.map +1 -1
  104. package/dist/esm/index.js +1 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +18 -1
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +18 -1
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/library-style.js +1 -1
  111. package/dist/esm/nextcloud-vue.css +712 -330
  112. package/dist/esm/utils/dueText.js +49 -8
  113. package/dist/esm/utils/dueText.js.map +1 -1
  114. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  115. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  116. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  117. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  118. package/dist/nextcloud-vue.cjs.js +8275 -6302
  119. package/dist/nextcloud-vue.cjs.js.map +2 -2
  120. package/dist/nextcloud-vue.css +712 -330
  121. package/l10n/en.json +18 -1
  122. package/l10n/nl.json +18 -1
  123. package/package.json +1 -1
  124. package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
  125. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
  126. package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
  127. package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
  128. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
  129. package/src/components/CnDashboardPage/CnDashboardPage.vue +96 -2
  130. package/src/components/CnDataTable/CnDataTable.md +1 -0
  131. package/src/components/CnDataTable/CnDataTable.vue +39 -10
  132. package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
  133. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
  134. package/src/components/CnDeviceFramesWidget/index.js +29 -0
  135. package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
  136. package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
  137. package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
  138. package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
  139. package/src/components/CnPageHeader/CnPageHeader.md +11 -0
  140. package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
  141. package/src/components/CnPagination/CnPagination.vue +10 -2
  142. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +84 -4
  143. package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
  144. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
  145. package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
  146. package/src/components/CnTabs/CnTabs.vue +39 -2
  147. package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
  148. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  149. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  150. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
  151. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +31 -2
  152. package/src/components/index.js +1 -0
  153. package/src/composables/indexSources.js +9 -0
  154. package/src/css/dialog.css +14 -0
  155. package/src/css/look-board-index.css +27 -1
  156. package/src/css/look-board.css +27 -0
  157. package/src/css/page-header.css +16 -0
  158. package/src/index.js +1 -0
  159. package/src/schemas/app-manifest-v2.schema.json +27 -15
  160. package/src/utils/dueText.js +49 -8
  161. package/src/utils/libraryWidgetKeys.js +1 -0
  162. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -74,6 +74,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
74
74
  title: $props.title,
75
75
  description: $options.resolvedDescription,
76
76
  icon: $props.icon,
77
+ breadcrumb: $props.breadcrumb,
78
+ meta: $props.headerMeta,
77
79
  visuallyHidden: !$props.showTitle
78
80
  }, createSlots({ _: 2 /* DYNAMIC */ }, [
79
81
  ($options.headerButtonsInHeader)
@@ -114,7 +116,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
114
116
  key: "0"
115
117
  }
116
118
  : undefined
117
- ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["title", "description", "icon", "visuallyHidden"]))
119
+ ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["title", "description", "icon", "breadcrumb", "meta", "visuallyHidden"]))
118
120
  : createCommentVNode("v-if", true)
119
121
  ]),
120
122
  ($options.headerButtonsOutside)
@@ -1 +1 @@
1
- {"version":3,"file":"CnSettingsPage.vue3.js","sources":["../../../../src/components/CnSettingsPage/CnSettingsPage.vue?vue&type=template&id=04e49f77&lang.js"],"sourcesContent":["<!--\n CnSettingsPage — Admin / config surface.\n\n Renders manifest-driven config sections. The page MAY declare\n EITHER a flat `sections[]` array (back-compat) OR a `tabs[]` array\n (each tab owns its own `sections[]`) — XOR, never both. See\n manifest-settings-orchestration spec.\n\n Each section is a CnSettingsCard wrapping a CnSettingsSection.\n A section MUST declare exactly one of three body kinds:\n\n 1. `fields[]` — flat form fields (back-compat, default)\n 2. `component` — registry-resolved component as the section body\n 3. `widgets[]` — ordered list of widgets (built-in + customComponents)\n\n Built-in widget types (resolved BEFORE the customComponents registry):\n - `version-info` → CnVersionInfoCard\n - `register-mapping` → CnRegisterMapping\n - `component` → discriminator; `widget.componentName`\n resolves against customComponents\n\n Saves via `axios.put(saveEndpoint, formData)` with the consumer's\n settings controller URL. Widget events bubble up through\n `@widget-event` so consumers wire one page-level handler that\n dispatches by `widgetType` + event `name` (the manifest can't carry\n inline JS).\n\n Supports the same `headerComponent` / `actionsComponent` / generic\n `slots` overrides that the other built-in page types do — the\n CnPageRenderer wires them via the `#header`, `#actions`, and named\n `slots[]` slots exposed here.\n-->\n<template>\n\t<div class=\"cn-settings-page\" data-testid=\"cn-settings-page\">\n\t\t<!-- Header — overridable via #header slot. CnPageHeader renders whenever\n\t\t there is a title: `visuallyHidden` keeps the <h1> in the\n\t\t accessibility tree (and out of the visual layout) when showTitle is\n\t\t false, so <main> is never left with no heading at all\n\t\t (WCAG 2.4.6 / 1.3.1). -->\n\t\t<slot\n\t\t\tname=\"header\"\n\t\t\t:title=\"title\"\n\t\t\t:description=\"description\"\n\t\t\t:icon=\"icon\">\n\t\t\t<CnPageHeader\n\t\t\t\tv-if=\"title\"\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"resolvedDescription\"\n\t\t\t\t:icon=\"icon\"\n\t\t\t\t:visuallyHidden=\"!showTitle\">\n\t\t\t\t<!-- Board look: the buildiq square, then the page Save as the\n\t\t\t\t last header button. -->\n\t\t\t\t<template v-if=\"headerButtonsInHeader\" #extra>\n\t\t\t\t\t<div class=\"cn-settings-page__header-buttons\" data-testid=\"cn-settings-page-header-buttons\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"isBoard\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"pageSave\"\n\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\t\t\t\tdata-testid=\"cn-settings-page-save\"\n\t\t\t\t\t\t\t@click=\"save\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ saveLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</template>\n\t\t\t</CnPageHeader>\n\t\t</slot>\n\n\t\t<!-- The same buttons when the header itself is hidden or replaced. -->\n\t\t<div\n\t\t\tv-if=\"headerButtonsOutside\"\n\t\t\tclass=\"cn-settings-page__header-buttons\"\n\t\t\tdata-testid=\"cn-settings-page-header-buttons\">\n\t\t\t<CnBuildiqEditButton v-if=\"isBoard\" />\n\t\t\t<NcButton\n\t\t\t\tv-if=\"pageSave\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\tdata-testid=\"cn-settings-page-save\"\n\t\t\t\t@click=\"save\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- The Changes card of a page that saves in its header. -->\n\t\t<div\n\t\t\tv-if=\"pageSave && changedFields.length > 0\"\n\t\t\tclass=\"cn-settings-page__changes\"\n\t\t\tdata-testid=\"cn-settings-page-changes\">\n\t\t\t<h2 class=\"cn-settings-page__changes-title\">\n\t\t\t\t{{ changesLabel }}\n\t\t\t</h2>\n\t\t\t<ul>\n\t\t\t\t<li v-for=\"field in changedFields\" :key=\"field.key\">\n\t\t\t\t\t{{ field.label }}\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\n\t\t<!-- Actions slot for save / discard / reset overrides -->\n\t\t<div v-if=\"$slots.actions || $slots.actions\" class=\"cn-settings-page__actions\">\n\t\t\t<slot name=\"actions\" />\n\t\t</div>\n\n\t\t<!-- Tab strip — only rendered when `tabs[]` is set\n\t\t\t (manifest-settings-orchestration REQ-MSO-5). Built with\n\t\t\t native <button role=\"tab\"> + ARIA wiring; uses Nextcloud\n\t\t\t CSS variables only (no hex literals). -->\n\t\t<div\n\t\t\tv-if=\"hasTabs\"\n\t\t\tclass=\"cn-settings-page__tabs\"\n\t\t\trole=\"tablist\"\n\t\t\tdata-testid=\"cn-settings-page-tabs\">\n\t\t\t<button\n\t\t\t\tv-for=\"(tab, tabIndex) in tabs\"\n\t\t\t\t:key=\"`tab-${tab.id}`\"\n\t\t\t\trole=\"tab\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-settings-page__tab\"\n\t\t\t\t:class=\"{ 'cn-settings-page__tab--active': activeTabId === tab.id }\"\n\t\t\t\t:aria-selected=\"activeTabId === tab.id ? 'true' : 'false'\"\n\t\t\t\t:aria-controls=\"`cn-settings-tab-panel-${tab.id}`\"\n\t\t\t\t:data-testid=\"`cn-settings-page-tab-${tab.id}`\"\n\t\t\t\t@click=\"onTabClick(tab, tabIndex)\">\n\t\t\t\t{{ resolveLabel(tab.label) }}\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- Sections — rendered for the active tab when `tabs[]` is\n\t\t\t set, otherwise the flat `sections[]` (back-compat). The wrapper\n\t\t\t draws no box of its own (`display: contents`); the board look\n\t\t\t turns it into the card grid. -->\n\t\t<div class=\"cn-settings-page__sections\">\n\t\t\t<CnSettingsCard\n\t\t\t\tv-for=\"(section, sectionIndex) in activeSections\"\n\t\t\t\t:key=\"`section-${activeTabId || 'flat'}-${sectionIndex}`\"\n\t\t\t\t:title=\"resolveLabel(section.title)\"\n\t\t\t\t:icon=\"section.icon || ''\"\n\t\t\t\t:collapsible=\"section.collapsible || false\">\n\t\t\t\t<!--\n\t\t\t\t\tBody resolution — see `sectionBodyKind(section)` in the\n\t\t\t\t\tscript: returns 'fields' (default + back-compat),\n\t\t\t\t\t'component', or 'widgets'.\n\t\t\t\t-->\n\n\t\t\t\t<!-- Body: bare fields[] (back-compat). Wrapped in a\n\t\t\t\t\t CnSettingsSection mirroring the pre-rich-sections layout. -->\n\t\t\t\t<CnSettingsSection\n\t\t\t\t\tv-if=\"sectionBodyKind(section) === 'fields'\"\n\t\t\t\t\t:name=\"resolveLabel(section.title)\"\n\t\t\t\t\t:description=\"resolveLabel(section.description)\"\n\t\t\t\t\t:docUrl=\"section.docUrl || ''\"\n\t\t\t\t\t:wide=\"Boolean(section.wide)\">\n\t\t\t\t\t<div class=\"cn-settings-page__fields\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tv-for=\"field in section.fields\"\n\t\t\t\t\t\t\t:key=\"field.key\"\n\t\t\t\t\t\t\tclass=\"cn-settings-page__field\">\n\t\t\t\t\t\t\t<!-- Per-field slot — `field-<key>` lets a consumer override the input entirely. -->\n\t\t\t\t\t\t\t<slot\n\t\t\t\t\t\t\t\t:name=\"`field-${field.key}`\"\n\t\t\t\t\t\t\t\t:field=\"field\"\n\t\t\t\t\t\t\t\t:value=\"formData[field.key]\"\n\t\t\t\t\t\t\t\t:onInput=\"(v) => updateField(field.key, v)\">\n\t\t\t\t\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t\t\t\t\tv-if=\"field.type === 'boolean'\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"!!formData[field.key]\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event)\">\n\t\t\t\t\t\t\t\t\t{{ resolveLabel(field.label) }}\n\t\t\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t\t\t<NcTextField\n\t\t\t\t\t\t\t\t\tv-else-if=\"field.type === 'number'\"\n\t\t\t\t\t\t\t\t\t:label=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"String(fieldValue(field.key, ''))\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event === '' ? null : Number($event))\" />\n\t\t\t\t\t\t\t\t<NcTextField\n\t\t\t\t\t\t\t\t\tv-else-if=\"field.type === 'password'\"\n\t\t\t\t\t\t\t\t\t:label=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"fieldValue(field.key, '')\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event)\" />\n\t\t\t\t\t\t\t\t<NcSelect\n\t\t\t\t\t\t\t\t\tv-else-if=\"field.type === 'enum' && Array.isArray(field.options)\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"selectedOption(field)\"\n\t\t\t\t\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t\t\t\t\t:inputLabel=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, optionValue($event))\" />\n\t\t\t\t\t\t\t\t<NcTextField\n\t\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\t\t:label=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"fieldValue(field.key, '')\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event)\" />\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t\t<small\n\t\t\t\t\t\t\t\tv-if=\"field.help\"\n\t\t\t\t\t\t\t\tclass=\"cn-settings-page__field-help\">\n\t\t\t\t\t\t\t\t{{ resolveLabel(field.help) }}\n\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<template v-if=\"sectionSave\" #footer>\n\t\t\t\t\t\t<div class=\"cn-settings-section__save\" data-testid=\"cn-settings-section-save\">\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\t\t\t\t\t@click=\"save\">\n\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t{{ saveLabel }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\t\t\t\t</CnSettingsSection>\n\n\t\t\t\t<!-- Body: a single registry-resolved component. The\n\t\t\t\t\t component is responsible for its own chrome — we do\n\t\t\t\t\t NOT wrap it in CnSettingsSection because the existing\n\t\t\t\t\t widget shape (CnVersionInfoCard, CnRegisterMapping)\n\t\t\t\t\t already wraps itself. Custom components are expected\n\t\t\t\t\t to do the same OR opt in by adding their own section\n\t\t\t\t\t wrapper.\n\n\t\t\t\t\t Wrapped in CnSettingsWidgetMount so the child's $emit\n\t\t\t\t\t is intercepted and re-emitted as @widget-event on this\n\t\t\t\t\t page (manifests can't carry inline JS). -->\n\t\t\t\t<CnSettingsWidgetMount\n\t\t\t\t\tv-else-if=\"sectionBodyKind(section) === 'component' && resolveSectionComponent(section)\"\n\t\t\t\t\t:component=\"resolveSectionComponent(section)\"\n\t\t\t\t\t:componentProps=\"section.props || {}\"\n\t\t\t\t\t:widgetType=\"section.component\"\n\t\t\t\t\t:sectionIndex=\"sectionIndex\"\n\t\t\t\t\t:widgetIndex=\"0\"\n\t\t\t\t\t@widgetEvent=\"onWidgetEvent\" />\n\n\t\t\t\t<!-- Body: ordered list of widgets. Each widget is its own\n\t\t\t\t\t mounted component with v-bind props + bubbled events\n\t\t\t\t\t via CnSettingsWidgetMount. Built-ins (version-info,\n\t\t\t\t\t register-mapping) wrap themselves in CnSettingsSection;\n\t\t\t\t\t custom widgets are expected to do the same. -->\n\t\t\t\t<template v-else-if=\"sectionBodyKind(section) === 'widgets'\">\n\t\t\t\t\t<CnSettingsWidgetMount\n\t\t\t\t\t\tv-for=\"entry in resolvedWidgetEntries(section, sectionIndex)\"\n\t\t\t\t\t\t:key=\"entry.key\"\n\t\t\t\t\t\t:component=\"entry.component\"\n\t\t\t\t\t\t:componentProps=\"entry.props\"\n\t\t\t\t\t\t:widgetType=\"entry.widgetType\"\n\t\t\t\t\t\t:sectionIndex=\"sectionIndex\"\n\t\t\t\t\t\t:widgetIndex=\"entry.widgetIndex\"\n\t\t\t\t\t\t@widgetEvent=\"onWidgetEvent\" />\n\t\t\t\t</template>\n\t\t\t</CnSettingsCard>\n\t\t</div>\n\n\t\t<!-- Save bar -->\n\t\t<div v-if=\"showSaveBar && !saveMode && !autosave\" class=\"cn-settings-page__save-bar\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\t@click=\"save\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"dirty\"\n\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t:disabled=\"saving\"\n\t\t\t\t@click=\"reset\">\n\t\t\t\t{{ resetLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Error -->\n\t\t<div v-if=\"lastError\" class=\"cn-settings-page__error\">\n\t\t\t{{ lastError }}\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport {\n\tNcButton,\n\tNcCheckboxRadioSwitch,\n\tNcLoadingIcon,\n\tNcSelect,\n\tNcTextField,\n} from '@nextcloud/vue'\nimport ContentSave from 'vue-material-design-icons/ContentSave.vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnRegisterMapping from '../CnRegisterMapping/CnRegisterMapping.vue'\nimport CnToggleTableWidget from '../CnToggleTableWidget/CnToggleTableWidget.vue'\nimport CnVersionInfoCard from '../CnVersionInfoCard/CnVersionInfoCard.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { CnPageHeader } from '../CnPageHeader/index.js'\nimport { CnSettingsCard } from '../CnSettingsCard/index.js'\nimport { CnSettingsSection } from '../CnSettingsSection/index.js'\nimport CnSettingsWidgetMount from './CnSettingsWidgetMount.js'\n\n/**\n * Sentinel value used in the built-in widget registry to mark the\n * `'component'` discriminator (manifest-settings-orchestration\n * REQ-MSO-6). The discriminator does NOT resolve to a fixed\n * component — instead, the resolver detects this sentinel and looks\n * up `widget.componentName` in the customComponents registry.\n */\nconst COMPONENT_DISCRIMINATOR = Symbol('cn-settings-component-widget')\n\n/**\n * Built-in widget registry. Used by `CnSettingsPage` to resolve\n * `widgets[].type` to a component BEFORE consulting the\n * consumer-provided customComponents registry.\n *\n * The order matters — built-ins win on collision so consumers can't\n * accidentally shadow `version-info` with their own component. If a\n * consumer needs to truly replace one of these, they can render their\n * own component via `section.component` or\n * `{ type: \"component\", componentName: <name> }` instead of `widgets[]`.\n *\n * Spec:\n * - REQ-MSRS-2 (manifest-settings-rich-sections) — fixed-component\n * built-ins (`version-info`, `register-mapping`).\n * - REQ-MSO-6 (manifest-settings-orchestration) — `'component'`\n * discriminator (sentinel value, resolved via componentName).\n */\nconst BUILTIN_SETTINGS_WIDGETS = Object.freeze({\n\t'version-info': CnVersionInfoCard,\n\t'register-mapping': CnRegisterMapping,\n\t// Rows with a switch: objects (`bind.mode: 'each'`) or a config map (`'map'`).\n\t'toggle-table': CnToggleTableWidget,\n\tcomponent: COMPONENT_DISCRIMINATOR,\n})\n\n/**\n * CnSettingsPage — Manifest-driven settings page.\n *\n * Reads `sections[]` from `pages[].config` and renders one\n * `CnSettingsCard` per section. Each section declares EXACTLY ONE of\n * three body kinds:\n *\n * - `fields[]` — flat form fields (back-compat default).\n * Each field's `type` controls which input renders:\n * - `boolean` → NcCheckboxRadioSwitch\n * - `number` → NcTextField (type=number)\n * - `string` → NcTextField (default)\n * - `password` → NcTextField (type=password)\n * - `enum` → NcSelect (requires `options: string[]`)\n * Per-field `#field-<key>` slots are exposed so consumers can drop\n * in their own widget.\n *\n * - `component` — registry-resolved component as the section body.\n * `section.props` is v-bind'd to it. Useful when a whole section\n * is one bespoke widget the library doesn't know about.\n *\n * - `widgets[]` — ordered list of widgets. Each entry has a `type`\n * string and optional `props`. Built-in widget types resolve\n * first (`version-info` → `CnVersionInfoCard`, `register-mapping`\n * → `CnRegisterMapping`, `toggle-table` → `CnToggleTableWidget`);\n * unknown types fall back to the\n * `customComponents` registry.\n *\n * Widget events (e.g. CnRegisterMapping's `@save`, CnVersionInfoCard's\n * `@update`) bubble up as a single `@widget-event` event on the page,\n * with payload `{ widgetType, widgetIndex, sectionIndex, name, args }`.\n * Consumers wire one handler at the CnAppRoot level and dispatch by\n * `widgetType` / `name` — the manifest can't carry inline JS, so this\n * is the documented event-wiring escape hatch.\n *\n * Saves via `axios.put(saveEndpoint, formData)`. The `saveEndpoint`\n * default is `/index.php/apps/{appId}/api/settings` — consumers should\n * pass a fully-qualified URL via the manifest (or `saveEndpoint` prop)\n * because the library has no knowledge of the consumer's app id at\n * import time.\n *\n * Slots:\n * - `#header` — Replaces the CnPageHeader.\n * - `#actions` — Right-aligned actions area (the renderer fills this\n * via `pages[].actionsComponent`).\n * - `#field-<key>` — Replaces the input for a specific field within\n * a `fields[]` section. Scope: `{ field, value, onInput }`.\n *\n * Events:\n * - `@save` — Emitted on successful save. Payload: the form data.\n * - `@error` — Emitted when save fails. Payload: the error.\n * - `@input` — Emitted on every field change. Payload: `{ key, value }`.\n * - `@widget-event` — Emitted when a widget mounted via `widgets[]`\n * or `component` re-emits one of its own events. Payload:\n * `{ widgetType, widgetIndex, sectionIndex, name, args }`.\n */\nexport default {\n\tname: 'CnSettingsPage',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcLoadingIcon,\n\t\tNcSelect,\n\t\tNcTextField,\n\t\tContentSave,\n\t\tCnBuildiqEditButton,\n\t\tCnSettingsCard,\n\t\tCnSettingsSection,\n\t\tCnPageHeader,\n\t\tCnSettingsWidgetMount,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Custom-component registry injected from CnAppRoot. Used to\n\t\t * resolve `section.component` and to fall back when a\n\t\t * `widgets[].type` isn't in the built-in widget map. Defaults\n\t\t * to an empty object when the page is mounted standalone.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The look the page is drawn in: `board` or `nextcloud`. Empty follows\n\t\t * the `cnLook` that CnAppRoot (or the page's `config.look`) provides.\n\t\t *\n\t\t * @spec openspec/changes/screens-chrome-parity/specs/app-look/spec.md#requirement-an-app-can-take-the-board-look\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Where the page saves (`config.saveMode`): `section` ends each section\n\t\t * that has fields in its own Save button, `page` draws one Save as the\n\t\t * last header button and a Changes card, and empty keeps the save bar\n\t\t * under the last section. Never both a section and a page save.\n\t\t *\n\t\t * @spec openspec/changes/screens-chrome-parity/specs/settings-board-look/spec.md#requirement-a-settings-page-saves-in-one-declared-place\n\t\t * @type {'section'|'page'|''}\n\t\t */\n\t\tsaveMode: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (value) => ['', 'section', 'page'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * The page saves on change (`config.autosave`): no save button at all,\n\t\t * and the description ends with \"Changes are saved automatically.\"\n\t\t *\n\t\t * @spec openspec/changes/screens-chrome-parity/specs/settings-board-look/spec.md#requirement-a-settings-page-saves-in-one-declared-place\n\t\t */\n\t\tautosave: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Page title. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Settings'),\n\t\t},\n\n\t\t/** Description shown under the title when `showTitle` is set. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether to render the inline page header VISIBLY. When false the\n\t\t * `<h1>` is still rendered visually-hidden, so the `<main>` landmark\n\t\t * always has an accessible heading (WCAG 2.4.6 / 1.3.1).\n\t\t */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** MDI icon name for the header. */\n\t\ticon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Section definitions (flat shape — back-compat). Each section\n\t\t * MUST declare EXACTLY ONE of:\n\t\t * - `fields: Array<Field>` (back-compat flat-field body)\n\t\t * - `component: <registry-name>` + optional `props`\n\t\t * - `widgets: Array<{ type, props?, componentName? }>`\n\t\t *\n\t\t * Common keys: `{ title, description?, icon?, collapsible?, docUrl? }`.\n\t\t *\n\t\t * Mutually exclusive with `tabs[]` (XOR — see\n\t\t * manifest-settings-orchestration REQ-MSO-1).\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Tab definitions (orchestration shape — manifest-settings-\n\t\t * orchestration REQ-MSO-2). When set, CnSettingsPage renders\n\t\t * a tab strip above the section area; the active tab's\n\t\t * `sections[]` flow into the same renderer used by the flat\n\t\t * shape. Mutually exclusive with `sections[]`.\n\t\t *\n\t\t * Each tab MUST be `{ id: string, label: string,\n\t\t * icon?: string, sections: array<Section> }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\ttabs: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Optional ID of the tab to activate on mount. When empty AND\n\t\t * `tabs[]` is non-empty, the first tab is active by default.\n\t\t * Unknown IDs fall back to the first tab.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tinitialTab: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Initial values keyed by `field.key`. Defaults to an empty\n\t\t * object; in practice the consumer passes the current\n\t\t * IAppConfig snapshot loaded from their settings controller.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tinitialValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Endpoint that receives the PUT on save. Pass a fully-qualified\n\t\t * URL — the library has no knowledge of the consumer's app id.\n\t\t * When empty, no PUT is issued and `@save` is emitted with the\n\t\t * form data so the consumer can persist it themselves.\n\t\t */\n\t\tsaveEndpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether to render the built-in save/reset bar. */\n\t\tshowSaveBar: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Label for the save button. */\n\t\tsaveLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Save'),\n\t\t},\n\n\t\t/** Label for the reset (discard) button. */\n\t\tresetLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Discard changes'),\n\t\t},\n\n\t\t/**\n\t\t * Optional translation function. When provided, applied to\n\t\t * section titles, field labels, and other i18n-key strings\n\t\t * declared in the manifest. Defaults to the identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit custom-component registry. When set, takes\n\t\t * precedence over the injected `cnCustomComponents`. Use this\n\t\t * when mounting CnSettingsPage outside a CnAppRoot tree (or\n\t\t * when test-mounting with a stub registry).\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcustomComponents: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['save', 'error', 'input', 'widget-event', 'tab-change'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\t// Resolve the initial active-tab id synchronously so the very\n\t\t// first render has the correct tab active (otherwise tests\n\t\t// that mount + assert without an `await tick` see the empty\n\t\t// default). Mirrors the logic in `resolveInitialTabId` (the\n\t\t// watcher path); keep them aligned.\n\t\tlet activeTabId = ''\n\t\tconst tabs = Array.isArray(this.tabs) ? this.tabs : []\n\t\tif (tabs.length > 0) {\n\t\t\tif (typeof this.initialTab === 'string' && this.initialTab.length > 0\n\t\t\t\t&& tabs.some((t) => t && t.id === this.initialTab)) {\n\t\t\t\tactiveTabId = this.initialTab\n\t\t\t} else if (tabs[0] && typeof tabs[0].id === 'string') {\n\t\t\t\tactiveTabId = tabs[0].id\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tformData: this.cloneInitial(),\n\t\t\toriginalData: this.cloneInitial(),\n\t\t\tsaving: false,\n\t\t\tlastError: null,\n\t\t\tactiveTabId,\n\t\t\tautosaveTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} The page saves from its header (`saveMode: \"page\"`, not autosave). */\n\t\tpageSave() {\n\t\t\treturn this.saveMode === 'page' && !this.autosave\n\t\t},\n\n\t\t/** @return {boolean} Each section with fields saves on its own (`saveMode: \"section\"`). */\n\t\tsectionSave() {\n\t\t\treturn this.saveMode === 'section' && !this.autosave\n\t\t},\n\n\t\t/**\n\t\t * The description, with the autosave note appended when the page saves\n\t\t * on change.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedDescription() {\n\t\t\tif (!this.autosave) {\n\t\t\t\treturn this.description\n\t\t\t}\n\t\t\tconst note = t('nextcloud-vue', 'Changes are saved automatically.')\n\t\t\treturn this.description ? `${this.description} ${note}` : note\n\t\t},\n\n\t\t/** @return {boolean} The header buttons render inside the visible page header. */\n\t\theaderButtonsInHeader() {\n\t\t\treturn Boolean(this.title && this.showTitle && !this.$slots.header && (this.isBoard || this.pageSave))\n\t\t},\n\n\t\t/** @return {boolean} The header buttons need a row of their own (header hidden or replaced). */\n\t\theaderButtonsOutside() {\n\t\t\treturn Boolean((this.isBoard || this.pageSave) && !this.headerButtonsInHeader)\n\t\t},\n\n\t\t/** @return {string} Title of the Changes card. */\n\t\tchangesLabel() {\n\t\t\treturn t('nextcloud-vue', 'Changes')\n\t\t},\n\n\t\t/**\n\t\t * The fields that differ from their saved value, across every section,\n\t\t * labelled for the Changes card.\n\t\t *\n\t\t * @return {Array<{key: string, label: string}>}\n\t\t */\n\t\tchangedFields() {\n\t\t\tconst labels = {}\n\t\t\tconst sections = [...(this.sections || [])]\n\t\t\tfor (const tab of this.tabs || []) {\n\t\t\t\tif (tab && Array.isArray(tab.sections)) {\n\t\t\t\t\tsections.push(...tab.sections)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const section of sections) {\n\t\t\t\tfor (const field of (section && section.fields) || []) {\n\t\t\t\t\tlabels[field.key] = this.resolveLabel(field.label) || field.key\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst keys = new Set([...Object.keys(this.formData), ...Object.keys(this.originalData)])\n\t\t\treturn [...keys]\n\t\t\t\t.filter((key) => JSON.stringify(this.formData[key]) !== JSON.stringify(this.originalData[key]))\n\t\t\t\t.map((key) => ({ key, label: labels[key] || key }))\n\t\t},\n\n\t\t/** Whether any field has changed since load. */\n\t\tdirty() {\n\t\t\treturn JSON.stringify(this.formData) !== JSON.stringify(this.originalData)\n\t\t},\n\n\t\t/**\n\t\t * Effective custom-component registry. Explicit prop wins over\n\t\t * the injected value (mirrors CnPageRenderer's resolution\n\t\t * order).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveCustomComponents() {\n\t\t\treturn this.customComponents ?? this.cnCustomComponents ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the page is in tabs orchestration mode. True when\n\t\t * `tabs[]` is non-empty — drives the tab-strip render gate\n\t\t * (manifest-settings-orchestration REQ-MSO-5).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasTabs() {\n\t\t\treturn Array.isArray(this.tabs) && this.tabs.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The sections to render right now. In flat mode, this is the\n\t\t * `sections` prop directly. In tabs mode, this is the\n\t\t * `sections[]` array of the currently active tab. Centralising\n\t\t * this in one computed keeps the template's `v-for` simple\n\t\t * and decouples it from the body kind dispatcher (which\n\t\t * applies per-section, not per-mode).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tactiveSections() {\n\t\t\tif (!this.hasTabs) {\n\t\t\t\treturn this.sections || []\n\t\t\t}\n\t\t\tconst active = this.tabs.find((t) => t && t.id === this.activeTabId)\n\t\t\tif (active && Array.isArray(active.sections)) {\n\t\t\t\treturn active.sections\n\t\t\t}\n\t\t\t// Defensive fallback — should not happen because\n\t\t\t// `resolveInitialTabId` always lands on a known tab.\n\t\t\tconst first = this.tabs[0]\n\t\t\treturn first && Array.isArray(first.sections) ? first.sections : []\n\t\t},\n\t},\n\n\twatch: {\n\t\tinitialValues: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.formData = this.cloneInitial()\n\t\t\t\tthis.originalData = this.cloneInitial()\n\t\t\t},\n\t\t},\n\n\t\t// When `tabs[]` changes (e.g. consumer swaps manifests at\n\t\t// runtime), re-resolve the active tab so the page doesn't get\n\t\t// stuck on a removed id.\n\t\ttabs: {\n\t\t\thandler() {\n\t\t\t\tthis.activeTabId = this.resolveInitialTabId()\n\t\t\t},\n\t\t},\n\n\t\t// When `initialTab` changes (consumer-controlled tab\n\t\t// activation), follow it.\n\t\tinitialTab(next) {\n\t\t\tif (typeof next === 'string' && next.length > 0) {\n\t\t\t\tconst exists = this.tabs.some((t) => t && t.id === next)\n\t\t\t\tif (exists) {\n\t\t\t\t\tthis.activeTabId = next\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tclearTimeout(this.autosaveTimer)\n\t},\n\n\tmethods: {\n\t\t// Coalesce a field value to a fallback when null / undefined.\n\t\t// Used in template bindings instead of `formData[key] ?? ''` because\n\t\t// vue-template-es2015-compiler (the Rollup VuePlugin's parser) does\n\t\t// not accept ES2020 nullish-coalescing inside template expressions.\n\t\tfieldValue(key, fallback) {\n\t\t\tconst v = this.formData[key]\n\t\t\treturn (v === null || v === undefined) ? fallback : v\n\t\t},\n\n\t\t// Resolve the currently-selected option for an enum field.\n\t\t// `field.options` may be an array of strings/numbers OR an array\n\t\t// of `{ label, value }`-shaped objects. NcSelect needs the actual\n\t\t// option entry (not the bare value) to render the selection\n\t\t// correctly, so this resolver looks up the matching object form\n\t\t// when a primitive value is stored. Returns `null` when nothing\n\t\t// is selected so NcSelect renders the empty state instead of\n\t\t// emitting a Vue warning about an undefined `value` prop.\n\t\tselectedOption(field) {\n\t\t\tconst v = this.formData[field.key]\n\t\t\tif (v === null || v === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (!Array.isArray(field.options)) {\n\t\t\t\treturn v\n\t\t\t}\n\t\t\tfor (const opt of field.options) {\n\t\t\t\tif (opt && typeof opt === 'object') {\n\t\t\t\t\tif (opt.value === v) {\n\t\t\t\t\t\treturn opt\n\t\t\t\t\t}\n\t\t\t\t} else if (opt === v) {\n\t\t\t\t\treturn opt\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn v\n\t\t},\n\n\t\t// Inverse of `selectedOption` — extract the storable value from\n\t\t// whatever NcSelect emits. NcSelect emits the full option entry\n\t\t// when options are objects, the primitive when options are\n\t\t// primitives, or `null` on clear.\n\t\toptionValue(emitted) {\n\t\t\tif (emitted === null || emitted === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof emitted === 'object' && 'value' in emitted) {\n\t\t\t\treturn emitted.value\n\t\t\t}\n\t\t\treturn emitted\n\t\t},\n\n\t\tcloneInitial() {\n\t\t\tconst merged = { ...(this.initialValues || {}) }\n\t\t\t// Collect every section across both modes (flat\n\t\t\t// `sections[]` AND `tabs[].sections[]`) so default values\n\t\t\t// are applied regardless of orchestration shape.\n\t\t\t// Only flat-field sections contribute defaults; component\n\t\t\t// and widgets sections own their own state.\n\t\t\tconst allSections = []\n\t\t\tfor (const section of this.sections || []) {\n\t\t\t\tallSections.push(section)\n\t\t\t}\n\t\t\tfor (const tab of this.tabs || []) {\n\t\t\t\tif (tab && Array.isArray(tab.sections)) {\n\t\t\t\t\tfor (const section of tab.sections) {\n\t\t\t\t\t\tallSections.push(section)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const section of allSections) {\n\t\t\t\tif (!section || !Array.isArray(section.fields)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor (const field of section.fields) {\n\t\t\t\t\tif (field.default !== undefined && merged[field.key] === undefined) {\n\t\t\t\t\t\tmerged[field.key] = field.default\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn merged\n\t\t},\n\n\t\tresolveLabel(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (this.translate) {\n\t\t\t\treturn this.translate(value)\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\tupdateField(key, value) {\n\t\t\tthis.formData[key] = value\n\t\t\tthis.$emit('input', { key, value })\n\t\t\tif (this.autosave) {\n\t\t\t\tthis.queueAutosave()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Determine which body kind a section declares. Returns\n\t\t * `'fields'` (default + back-compat), `'component'`, or\n\t\t * `'widgets'`. The validator (`validateManifest`) enforces\n\t\t * exactly-one at parse time; this method is the runtime\n\t\t * equivalent and is permissive when one of the keys is set\n\t\t * (last-defined wins in the rare case of an unvalidated\n\t\t * manifest).\n\t\t *\n\t\t * @param {object} section A section entry from `sections[]`.\n\t\t * @return {'fields' | 'component' | 'widgets'}\n\t\t */\n\t\tsectionBodyKind(section) {\n\t\t\tif (Array.isArray(section.widgets) && section.widgets.length > 0) {\n\t\t\t\treturn 'widgets'\n\t\t\t}\n\t\t\tif (typeof section.component === 'string' && section.component.length > 0) {\n\t\t\t\treturn 'component'\n\t\t\t}\n\t\t\treturn 'fields'\n\t\t},\n\n\t\t/**\n\t\t * Resolve a `section.component` registry name to a Vue\n\t\t * component via `effectiveCustomComponents`. Returns `null`\n\t\t * (and warns) when the registry has no entry.\n\t\t *\n\t\t * @param {object} section A section entry.\n\t\t * @return {object|null} Vue component or null.\n\t\t */\n\t\tresolveSectionComponent(section) {\n\t\t\tconst name = section.component\n\t\t\tif (!name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = this.effectiveCustomComponents[name]\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnSettingsPage] Section component \"${name}\" not found in customComponents registry. Section body will be empty.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/**\n\t\t * Resolve a single `widgets[]` entry to a concrete Vue\n\t\t * component. Lookup order:\n\t\t *\n\t\t * 1. Built-in widget map (`version-info`, `register-mapping`).\n\t\t * 2. `'component'` discriminator (REQ-MSO-6) — resolves\n\t\t * `widget.componentName` against `effectiveCustomComponents`.\n\t\t * 3. Legacy fallback — looks up `widget.type` against\n\t\t * `effectiveCustomComponents`. Kept for back-compat with\n\t\t * manifest-settings-rich-sections consumers; flagged as\n\t\t * deprecated in JSDoc — manifest authors should migrate to\n\t\t * the explicit `{ type: \"component\", componentName }` shape.\n\t\t *\n\t\t * Returns `null` (and warns) when nothing resolves. Built-ins\n\t\t * win on collision so consumers can't accidentally shadow them.\n\t\t *\n\t\t * @param {object} widget A `widgets[]` entry, e.g. `{ type, props?, componentName? }`.\n\t\t * @return {object|null} Vue component or null.\n\t\t */\n\t\tresolveWidgetComponent(widget) {\n\t\t\tconst type = widget && typeof widget.type === 'string' ? widget.type : ''\n\t\t\tif (!type) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (Object.hasOwn(BUILTIN_SETTINGS_WIDGETS, type)) {\n\t\t\t\tconst builtin = BUILTIN_SETTINGS_WIDGETS[type]\n\t\t\t\tif (builtin === COMPONENT_DISCRIMINATOR) {\n\t\t\t\t\t// REQ-MSO-6: discriminator — look up `componentName`.\n\t\t\t\t\tconst name = widget.componentName\n\t\t\t\t\tif (typeof name !== 'string' || name.length === 0) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn('[CnSettingsPage] Widget {type:\"component\"} requires a non-empty `componentName`. Widget will be skipped.')\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\tconst resolved = this.effectiveCustomComponents[name]\n\t\t\t\t\tif (!resolved) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`[CnSettingsPage] Widget component \"${name}\" not found in customComponents registry. Widget will be skipped.`)\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\treturn resolved\n\t\t\t\t}\n\t\t\t\treturn builtin\n\t\t\t}\n\t\t\t// Legacy fallback (manifest-settings-rich-sections REQ-MSRS-2).\n\t\t\t// Deprecated — manifest authors should migrate to\n\t\t\t// `{ type: \"component\", componentName: <X> }`. Kept here so\n\t\t\t// existing consumers continue working unchanged.\n\t\t\tconst resolved = this.effectiveCustomComponents[type]\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnSettingsPage] Widget type \"${type}\" not found in built-in widgets or customComponents registry. Widget will be skipped.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/**\n\t\t * Build the list of widget mount entries for a `widgets[]`\n\t\t * section. Filters out entries whose `widget.type` doesn't\n\t\t * resolve (built-in OR customComponents) — `resolveWidgetComponent`\n\t\t * has already logged a warn. The filter happens here so the\n\t\t * template can use a clean `v-for` without nested `v-if`.\n\t\t *\n\t\t * The `widgetType` carried on the bubbled `@widget-event`\n\t\t * payload is the widget's `componentName` (when the\n\t\t * discriminator is `'component'`) or `widget.type` otherwise —\n\t\t * giving consumers a stable identifier for the dispatch.\n\t\t *\n\t\t * @param {object} section A section entry with `widgets[]`.\n\t\t * @param {number} sectionIndex Index in `sections[]`.\n\t\t * @return {Array<{key: string, component: object, props: object, widgetType: string, widgetIndex: number}>}\n\t\t */\n\t\tresolvedWidgetEntries(section, sectionIndex) {\n\t\t\tconst entries = []\n\t\t\tconst widgets = Array.isArray(section.widgets) ? section.widgets : []\n\t\t\tfor (let widgetIndex = 0; widgetIndex < widgets.length; widgetIndex++) {\n\t\t\t\tconst widget = widgets[widgetIndex] || {}\n\t\t\t\tconst component = this.resolveWidgetComponent(widget)\n\t\t\t\tif (!component) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst widgetType = widget.type === 'component' && typeof widget.componentName === 'string'\n\t\t\t\t\t? widget.componentName\n\t\t\t\t\t: widget.type\n\t\t\t\tentries.push({\n\t\t\t\t\tkey: `widget-${sectionIndex}-${widgetIndex}`,\n\t\t\t\t\tcomponent,\n\t\t\t\t\tprops: widget.props || {},\n\t\t\t\t\twidgetType,\n\t\t\t\t\twidgetIndex,\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn entries\n\t\t},\n\n\t\t/**\n\t\t * Resolve the active-tab id on mount / when `tabs[]` changes.\n\t\t * Lookup order: explicit `initialTab` prop → first tab in\n\t\t * `tabs[]` → empty string. Unknown `initialTab` values fall\n\t\t * back to the first tab so the page never gets stuck.\n\t\t * (manifest-settings-orchestration REQ-MSO-5.)\n\t\t *\n\t\t * @return {string} The resolved tab id (empty in flat mode).\n\t\t */\n\t\tresolveInitialTabId() {\n\t\t\tif (!this.hasTabs) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (typeof this.initialTab === 'string' && this.initialTab.length > 0) {\n\t\t\t\tconst exists = this.tabs.some((t) => t && t.id === this.initialTab)\n\t\t\t\tif (exists) {\n\t\t\t\t\treturn this.initialTab\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst first = this.tabs[0]\n\t\t\treturn first && typeof first.id === 'string' ? first.id : ''\n\t\t},\n\n\t\t/**\n\t\t * Handle a tab button click. Switches the active tab and\n\t\t * emits `@tab-change` so consumers can react (e.g. persist the\n\t\t * active tab in their preference store, update the URL hash).\n\t\t * (manifest-settings-orchestration REQ-MSO-5.)\n\t\t *\n\t\t * @param {object} tab The clicked tab definition.\n\t\t * @param {number} tabIndex The tab's index in `tabs[]`.\n\t\t */\n\t\tonTabClick(tab, tabIndex) {\n\t\t\tif (!tab || typeof tab.id !== 'string') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.activeTabId === tab.id) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.activeTabId = tab.id\n\t\t\tthis.$emit('tab-change', { tabId: tab.id, tabIndex })\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a widget's `widget-event` (caught by the local\n\t\t * CnSettingsWidgetMount helper) as a top-level `widget-event`\n\t\t * on this page. Consumers wire one `@widget-event` handler at\n\t\t * the CnAppRoot mount point and dispatch by `widgetType` /\n\t\t * `name`.\n\t\t *\n\t\t * @param {object} payload `{ widgetType, widgetIndex, sectionIndex, name, args }`\n\t\t */\n\t\tonWidgetEvent(payload) {\n\t\t\tthis.$emit('widget-event', payload)\n\t\t},\n\n\t\t/**\n\t\t * Save shortly after the last change (`autosave`): a burst of typing\n\t\t * is one request.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tqueueAutosave() {\n\t\t\tclearTimeout(this.autosaveTimer)\n\t\t\tthis.autosaveTimer = setTimeout(() => {\n\t\t\t\tthis.autosaveTimer = null\n\t\t\t\tif (this.dirty && !this.saving) {\n\t\t\t\t\tthis.save()\n\t\t\t\t}\n\t\t\t}, 600)\n\t\t},\n\n\t\treset() {\n\t\t\tthis.formData = this.cloneInitial()\n\t\t\tthis.lastError = null\n\t\t},\n\n\t\tasync save() {\n\t\t\tthis.saving = true\n\t\t\tthis.lastError = null\n\t\t\ttry {\n\t\t\t\tif (this.saveEndpoint) {\n\t\t\t\t\tawait axios.put(prefixUrl(this.saveEndpoint), this.formData)\n\t\t\t\t}\n\t\t\t\tthis.originalData = JSON.parse(JSON.stringify(this.formData))\n\t\t\t\tthis.$emit('save', this.formData)\n\t\t\t} catch (err) {\n\t\t\t\tthis.lastError = err?.response?.data?.message || err?.message || t('nextcloud-vue', 'Save failed')\n\t\t\t\tthis.$emit('error', err)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The section wrapper draws no box; the board look makes it the card grid. */\n.cn-settings-page__sections {\n\tdisplay: contents;\n}\n\n.cn-settings-page__header-buttons {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmargin-inline-start: auto;\n}\n\n.cn-settings-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.cn-settings-page__actions {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n}\n\n/*\n * Tab strip for the orchestration shape (manifest-settings-\n * orchestration REQ-MSO-5). Uses Nextcloud CSS variables only — no\n * hex literals, no rgba overrides on the elements themselves.\n */\n.cn-settings-page__tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px;\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding-bottom: 0;\n\tmargin-bottom: 8px;\n}\n\n.cn-settings-page__tab {\n\tbackground: transparent;\n\tborder: 0;\n\tborder-bottom: 3px solid transparent;\n\tpadding: 8px 16px;\n\tmargin: 0;\n\tcursor: pointer;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: normal;\n\tborder-radius: var(--border-radius-large) var(--border-radius-large) 0 0;\n\ttransition: background-color 0.1s ease-in-out, color 0.1s ease-in-out, border-color 0.1s ease-in-out;\n}\n\n.cn-settings-page__tab:hover,\n.cn-settings-page__tab:focus-visible {\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n\toutline: none;\n}\n\n.cn-settings-page__tab--active {\n\tcolor: var(--color-primary-element);\n\tborder-bottom-color: var(--color-primary-element);\n\tfont-weight: bold;\n}\n\n@media (max-width: 768px) {\n\t.cn-settings-page__tabs {\n\t\tflex-direction: column;\n\t\tgap: 0;\n\t\tborder-bottom: 0;\n\t}\n\n\t.cn-settings-page__tab {\n\t\tborder-bottom: 1px solid var(--color-border);\n\t\tborder-radius: 0;\n\t\ttext-align: left;\n\t}\n\n\t.cn-settings-page__tab--active {\n\t\tborder-bottom-width: 1px;\n\t\tbackground-color: var(--color-background-hover);\n\t}\n}\n\n.cn-settings-page__fields {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-settings-page__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-settings-page__field-help {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-page__save-bar {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-top: 16px;\n}\n\n.cn-settings-page__error {\n\tcolor: var(--color-error);\n\tbackground: var(--color-error-light, var(--color-background-hover));\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_createTextVNode","_openBlock","_createElementBlock","_toDisplayString","_renderSlot","_Fragment","_renderList","_createVNode"],"mappings":";;;OAiEW,EAAC,kBAAS;AAAA,EAAA,aAAA,EAAA,kBAAA;;;AAoBX,EAAA,KAAK,EAAC,kCAA8B;AAAA,EAAA,aAAA,EAAA,iCAAA;;;;AAsB3C,EAAA,KAAK,EAAC,kCAAkC;AACxC,EAAA,aAAW,EAAC,iCAAiC;;;;AAmB5C,EAAA,KAAI,EAAC,2BAA2B;eACtB,EAAC,0BAA0B;;AAClC,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,iCAAiC,GAAA;;;AAWL,EAAA,KAAA,EAAA,2BAAA;;;;OAUjC,EAAC,wBAAqB;AAC1B,EAAA,IAAE,EAAA,SAAA;;;;AAqBE,MAAA,UAAA,GAAA,EAAA,KAAI,EAAC,4BAAgB,GAAA;AAqBnB,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,0BAAgB,GAAA;;;AA2CxB,EAAA,KAAK,EAAC,8BAAW;;;AAMd,EAAA,KAAK,EAAC,2BAAG;AAAA,EAAA,aAAA,EAAA,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;cA5IX,CAAA,IAAA,CAAA,MAAA,EAAA,QAAA,EAAA;AA7BL,MAAA,KAAC,EAAA,MAAA,CAAA,KAAA;AACD,MAAA,WAAW,EAAE,MAAW,CAAA,WAAA;AACxB,MAAA,IAAI,EAAE,MAAI,CAAA,IAAA;aA2BL;OAzBC,MAAK,CAAA,KAAA;wBADZA,WAyBe,CAAA,uBAAA,EAAA;;AAvBb,YAAA,KAAK,EAAC,MAAA,CAAA,KAAA;AACN,YAAA,WAAW,EAAE,QAAA,CAAA,mBAAA;AACb,YAAA,IAAI,EAAE,MAAI,CAAA,IAAA;AACV,YAAA,cAAc,GAAG,MAAS,CAAA,SAAA;;aAGZ,QAAA,CAAA,qBAAA;;wBAAA,OAAA;8BACd,MAcE;AAdF,oBAAAC,kBAAA,CAcE,OAdF,UAcE,EAAA;uBAb0B,MAAI,CAAA,OAAA;wCAA/BD,WAA+B,CAAA,8BAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;;uBAExB,QAAK,CAAA,QAAA;wCADZA,WAWW,CAAA,mBAAA,EAAA;;AATV,4BAAA,OAAM,EAAA,SAAA;4BACN,QAAA,EAAA,KAAA,CAAA,MAAA,IAAA,CAAA,QAAA,CAAA,KAAA;;AAEC,4BAAA,OAAK,EAAE,QAAI,CAAA,IAAA;;AACd,4BAAA,IAAA,UACG,MAAqC;+BAAhB,KAAM,CAAA,MAAA;gDAA3BA,WAAqC,CAAA,wBAAA,EAAA;;AAAP,oCAAA,IAAI,EAAE,EAAC;;gDACrCA,WAA2B,CAAA,sBAAA,EAAA;;AAAN,oCAAA,IAAI,EAAE,EAAA;;;6CACjB,MACX;AADW,8BAAAE,eAAA,CAAA,GACX,mBAAG,MAAS,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;AAQjB,SAAAC,SAAA,EAAA,EAAAC,kBAAA,CAiBM,OAjBN,UAiBM,EAAA;WAbiB,MAAA,CAAA,OAAA;4BAAtBJ,WAAsB,CAAA,8BAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;;;4BACtBA,WAWM,CAAA,mBAAA,EAAA;;AATL,gBAAA,OAAO,EAAC,SAAS;gBAChB,QAAQ,EAAE,KAAM,CAAA,MAAA,IAAA,CAAK,QAAK,CAAA,KAAA;AAC3B,gBAAA,aAAW,EAAC,uBAAuB;AAClC,gBAAA,OAAK,EAAE,QAAI,CAAA,IAAA;;AACX,gBAAA,IAAA,UACA,MAAW;mBAAA,KAAA,CAAA,MAAA;oCAAXA,WAAW,CAAA,wBAAA,EAAA;;AAAA,wBAAA,IAAA,EAAA,EAAA;;oCACXA,WAA2B,CAAA,sBAAA,EAAA;;AAAN,wBAAA,IAAI,EAAE,EAAA;;;iCACjB,MACX;AADW,kBAAAE,eAAA,CAAA,GACX,mBAAG,MAAS,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;KAMP,QAAQ,CAAA,QAAA,IAAI,QAAW,CAAA,aAAA,CAAA,MAAA,GAAA,CAAA;yCAWzB,iBAAA,EAAA;4BANC,CAAA,IAAA,YAAA,EAAAG,eAAA,CADD,QAAY,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;4BAMX,CAAA,IAAA,EAAA,IAAA,EAAA;8BAHJD,8CAAO,QAAA,CAAA,aAAA,EAAA,CAAA,KAAA,KAAA;mCAAPA;gBAAO,GAAA,EAAA,KAAA,CAAA,GAAA;AACJ,eAAA,EAAAC,eAAA,CAAA,KAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;;;;AAMM,IAAA,CAAA,IAAA,CAAA,MAAM,CAAC,OAAO,IAAI,IAAA,CAAA,MAAM,CAAC,OAAG;AAAvC,SAAAF,SAAA,EAAA,EAAAC,kBAAA,CAEM,OAFN,UAEM,EAAA;UADLE,UAAc,CAAA,IAAA,CAAA,MAAA,EAAA,SAAA,CAAA;;;KAQR,QAAO,CAAA,OAAA;yCAiBR,iBAAA,EAAA;AAbJ,WAAAH,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CAYQG,QAXkB,EAAA,IAAA,EAAAC,UAAA,CAAA,MAAA,CAAA,IAAI,EAAtB,CAAA,GAAG,EAAE,QAAQ,KAAA;iCADrBJ,kBAYQ,CAAA,QAAA,EAAA;cAVP,GAAG,EAAA,CAAA,IAAA,EAAS,GAAG,CAAC,EAAA,CAAA,CAAA;AAChB,cAAA,IAAG,EAAC,KAAK;AACT,cAAA,IAAG,EAAC,QAAQ;mBACR,kBAAC,uBAAuB,EAAA,EAAA,+BAAA,EACC,iBAAA,KAAA,GAAA,CAAA,EAAA,EAAA,CAAA,CAAA;AAC7B,cAAA,eAAa,EAAE,KAAA,CAAA,WAAW,KAAG,GAAA,CAAA,EAAA,GAAA,MAAA,GAAA,OAAA;cAC7B,eAAa,EAAA,CAAA,sBAAA,EAAwB,GAAA,CAAA,EAAA,CAAA,CAAA;cACpC,aAAG,EAAA,CAAA,qBAAA,EAAA,GAAA,CAAA,EAAA,CAAA,CAAA;qBACC,EAAE,MAAA,KAAA,QAAA,CAAA,UAAS,CAAE,GAAG,EAAE,QAAQ,CAAA,CAAA;+BAC7B,QAAY,CAAA,YAAA,CAAC,GAAE,CAAA,KAAA,CAAA,CAAA,EAAA,EAAA,2BAAA,UAAA,CAAA,CAAA;;;;AAQd,IAAAH,kBAAA,QAAA;mDAE8B,EAAA,IAAA,EAAAO,UAAA,CAAA,QAAA,CAAA,cAAK,EAA/B,CAAA,OAAO,EAAE,YAAY,KAAA;;0BACZ,KAAW,CAAA,WAAA,IAAA,MAAA,CAAA,CAAA,EAAc,YAAY,CAAA,CAAA;eAChD,EAAE,QAAA,CAAA,YAAY,CAAC,OAAO,CAAC,KAAK,CAAA;cAC7B,EAAE,OAAO,CAAC,IAAI,IAAA,EAAA;qBACP,EAAE,OAAC,CAAA,WAAA,IAAA,KAAA;;2BASV,MAoEe;AAnEb,YAAA,CAAA,QAAA,CAAA,eAAe,CAAC,OAAO,CAAA,KAAA,QAAA;8BADzBR,WAoEe,CAAA,4BAAA,EAAA;;sBAlEd,EAAE,QAAA,CAAA,YAAY,CAAC,OAAO,CAAC,KAAK,CAAA;AAC9B,kBAAA,WAAQ,EAAA,QAAA,CAAA,YAAA,CAAA,OAAA,CAAA,WAAA,CAAA;wBACJ,EAAE,OAAO,CAAC,MAAM,IAAA,EAAA;AACnB,kBAAA,IAAC,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAA;;yCAgDrB;uCAAA,kBAAA,EAAA;AA9CF,uBAAAG,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CA6CGG,QA5CW,EAAA,IAAA,EAAAC,UAAA,CAAA,OAAO,CAAC,MAAM,GAAvB,KAAK,KAAA;6CADVJ,kBA6CG,CAAA,KAAA,EAAA;0BA3CJ,GAAG,EAAE,KAAK,CAAC,GAAA;+BACP,EAAC,yBAAC;;0BAELE,UAkCK,CAAA,IAAA,CAAA,MAAA,EAAA,CAAA,MAAA,EAjCU,KAAK,CAAC,GAAG,CAAA,CAAA,EAAA;AACxB,4BAAA,KAAI,EAAA,KAAA;AACJ,4BAAA,KAAG,EAAA,KAAA,CAAA,QAAA,CAAA,KAAA,CAAA,GAAA,CAAA;4BACH,OAAO,EAAA,CAAG,CAAC,KAAI,QAAA,CAAA,WAAA,CAAA,KAAA,CAAA,GAAA,EAAA,CAAA,CAAA;6BAJf,MAkCK;AA5BC,4BAAA,CAAA,KAAK,CAAC,IAAI,KAAA,SAAA;8CADjBN,WAKwB,CAAA,gCAAA,EAAA;;AAHtB,kCAAA,UAAI,EAAA,CAAA,CAAA,KAAA,CAAA,QAAA,CAAA,KAAA,CAAA,GAAA,CAAA;AACJ,kCAAA,qBAAC,aAAA,QAAA,CAAA,WAAA,CAAA,KAAA,CAAA,GAAA,EAAA,MAAA,CAAA,CAAA;;mDACF,MAAC;oEAAA,QAAA,CAAA,YAAA,CAAA,KAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;AAGK,gCAAA,CAAA,KAAA,CAAA,IAAA,KAAA,QAAA;gDADPA,WAKe,CAAA,sBAAA,EAAA;;AAHb,oCAAA,KAAK,EAAE,QAAA,CAAA,YAAO,CAAA,KAAA,CAAA,KAAA,CAAA;AACf,oCAAA,IAAI,EAAC,QAAQ;AACZ,oCAAA,UAAU,EAAC,MAAA,CAAA,mBAAA,CAAA,KAAA,CAAA,GAAA,EAAA,EAAA,CAAA,CAAA;oCACX,qBAAa,EAAA,MAAA,KAAA,QAAA,CAAA,WAAA,CAAA,KAAA,CAAA,GAAA,EAAA,MAAA,KAAA,EAAA,GAAA,IAAA,GAAA,MAAA,CAAA,MAAA,CAAA,CAAA,CAAA;;AAEH,kCAAA,CAAA,KAAK,CAAC,IAAI,KAAA,UAAA;kDADrBA,WAKE,CAAA,sBAAA,EAAA;;AAHD,sCAAA,KAAK,EAAE,QAAA,CAAA,YAAC,CAAA,KAAA,CAAA,KAAA,CAAA;AACT,sCAAA,IAAI,EAAC,UAAQ;AACZ,sCAAA,UAAO,EAAA,QAAA,CAAA,UAAA,CAAA,KAAA,CAAA,GAAA,EAAA,EAAA,CAAA;AACP,sCAAA,qBAAC,aAAA,QAAA,CAAA,WAAA,CAAA,KAAA,CAAA,GAAA,EAAA,MAAA,CAAA,CAAA;;qCAES,KAAG,CAAA,IAAA,KAAA,MAAA,IAAA,KAAA,CAAA,OAAA,CAAA,KAAA,CAAA,OAAA,CAAA;oDADfA,WAKkD,CAAA,mBAAA,EAAA;;;wCAFhD,OAAO,EAAE,KAAK,CAAC,OAAO;AACtB,wCAAA,UAAU,EAAE,QAAA,CAAA,YAAW,CAAE,KAAK,CAAC,KAAK,CAAA;wCACpC,qBAAiB,EAAA,MAAA,KAAE,oBAAW,CAAC,MAAM,KAAK,QAAM,CAAA,WAAA,CAAA,MAAA,CAAA,CAAA,CAAA;;oDAClDA;;AAEE,wCAAA,OAAO,QAAA,CAAA,YAAY,MAAM,CAAC,KAAK,CAAA;AAC/B,wCAAA,UAAG,EAAA,QAAA,CAAA,UAAA,CAAA,KAAA,CAAA,GAAA,EAAA,EAAA,CAAA;;;;AAIC,0BAAA,CAAA,KAAK,CAAC,IAAI;AADjB,+BAAAG,SAAA,EAAA,EAAAC,kBAAA,CAIQ,SAJR,WAIQ,EAAAC,eAAA,CADJ,qBAAY,CAAC,KAAK,CAAC,IAAI,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;mBAIb,QAAW,CAAA,WAAA;;8BAAA,QAAA;oCAC1B,MAWM;AAXN,0BAAAJ,kBAAA,CAWM,OAXN,WAWM,EAAA;4BAVLQ,WASY,CAAA,mBAAA,EAAA;;8BAPV,QAAQ,EAAE,KAAM,CAAA,MAAA,IAAA,CAAM,QAAI,CAAA,KAAA;AAC1B,8BAAA,OAAK,EAAE,QAAI,CAAA,IAAA;;AACD,8BAAA,IAAI,UACd,MAA0C;iCAArB,KAAM,CAAA,MAAA;kDAA3BT,WAA0C,CAAA,wBAAA,EAAA;;AAAb,sCAAA,IAAK,EAAE,EAAE;;kDACtCA,WAAiC,CAAA,sBAAA,EAAA;;AAAZ,sCAAA,IAAI,EAAC,EAAG;;;+CACnB,MACX;AADW,gCAAAE,eAAA,CAAA,GACX,mBAAG,MAAS,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;AAkBJ,gBAAA,CAAA,QAAA,CAAA,eAAC,CAAA,OAAA,CAAA,KAAA,WAAA,IAAA,QAAA,CAAA,uBAAA,CAAA,OAAA,CAAA;gCADbF,WAOS,CAAA,gCAAA,EAAA;;oBALP,SAAS,EAAE,QAAE,CAAA,uBAAA,CAAA,OAAA,CAAA;oBACb,cAAc,EAAE,OAAO,CAAC,KAAK,IAAA,EAAA;oBAC7B,UAAU,EAAE,OAAO,CAAC,SAAQ;AAC5B,oBAAA,YAAQ,EAAA,YAAA;AACT,oBAAA,WAAY,EAAC,CAAA;AACZ,oBAAA,aAAO,EAAA,QAAA,CAAA,aAAA;;AAOA,kBAAA,CAAA,QAAA,CAAA,eAAA,CAAA,OAAA,CAAA,KAAA,SAAA;AACR,qBAAAG,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA;2CAAAJ;wBAEE,GAAG,EAAG,KAAE,CAAA,GAAA;wBACR,SAAS,EAAE,KAAM,CAAA,SAAS;wBAC1B,cAAU,EAAA,KAAA,CAAA,KAAA;wBACV,UAAA,EAAA,KAAA,CAAA,UAAA;AACD,wBAAA,YAAA,EAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnSettingsPage.vue3.js","sources":["../../../../src/components/CnSettingsPage/CnSettingsPage.vue?vue&type=template&id=50865bde&lang.js"],"sourcesContent":["<!--\n CnSettingsPage — Admin / config surface.\n\n Renders manifest-driven config sections. The page MAY declare\n EITHER a flat `sections[]` array (back-compat) OR a `tabs[]` array\n (each tab owns its own `sections[]`) — XOR, never both. See\n manifest-settings-orchestration spec.\n\n Each section is a CnSettingsCard wrapping a CnSettingsSection.\n A section MUST declare exactly one of three body kinds:\n\n 1. `fields[]` — flat form fields (back-compat, default)\n 2. `component` — registry-resolved component as the section body\n 3. `widgets[]` — ordered list of widgets (built-in + customComponents)\n\n Built-in widget types (resolved BEFORE the customComponents registry):\n - `version-info` → CnVersionInfoCard\n - `register-mapping` → CnRegisterMapping\n - `component` → discriminator; `widget.componentName`\n resolves against customComponents\n\n Saves via `axios.put(saveEndpoint, formData)` with the consumer's\n settings controller URL. Widget events bubble up through\n `@widget-event` so consumers wire one page-level handler that\n dispatches by `widgetType` + event `name` (the manifest can't carry\n inline JS).\n\n Supports the same `headerComponent` / `actionsComponent` / generic\n `slots` overrides that the other built-in page types do — the\n CnPageRenderer wires them via the `#header`, `#actions`, and named\n `slots[]` slots exposed here.\n-->\n<template>\n\t<div class=\"cn-settings-page\" data-testid=\"cn-settings-page\">\n\t\t<!-- Header — overridable via #header slot. CnPageHeader renders whenever\n\t\t there is a title: `visuallyHidden` keeps the <h1> in the\n\t\t accessibility tree (and out of the visual layout) when showTitle is\n\t\t false, so <main> is never left with no heading at all\n\t\t (WCAG 2.4.6 / 1.3.1). -->\n\t\t<slot\n\t\t\tname=\"header\"\n\t\t\t:title=\"title\"\n\t\t\t:description=\"description\"\n\t\t\t:icon=\"icon\">\n\t\t\t<CnPageHeader\n\t\t\t\tv-if=\"title\"\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"resolvedDescription\"\n\t\t\t\t:icon=\"icon\"\n\t\t\t\t:breadcrumb=\"breadcrumb\"\n\t\t\t\t:meta=\"headerMeta\"\n\t\t\t\t:visuallyHidden=\"!showTitle\">\n\t\t\t\t<!-- Board look: the buildiq square, then the page Save as the\n\t\t\t\t last header button. -->\n\t\t\t\t<template v-if=\"headerButtonsInHeader\" #extra>\n\t\t\t\t\t<div class=\"cn-settings-page__header-buttons\" data-testid=\"cn-settings-page-header-buttons\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"isBoard\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"pageSave\"\n\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\t\t\t\tdata-testid=\"cn-settings-page-save\"\n\t\t\t\t\t\t\t@click=\"save\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ saveLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</template>\n\t\t\t</CnPageHeader>\n\t\t</slot>\n\n\t\t<!-- The same buttons when the header itself is hidden or replaced. -->\n\t\t<div\n\t\t\tv-if=\"headerButtonsOutside\"\n\t\t\tclass=\"cn-settings-page__header-buttons\"\n\t\t\tdata-testid=\"cn-settings-page-header-buttons\">\n\t\t\t<CnBuildiqEditButton v-if=\"isBoard\" />\n\t\t\t<NcButton\n\t\t\t\tv-if=\"pageSave\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\tdata-testid=\"cn-settings-page-save\"\n\t\t\t\t@click=\"save\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- The Changes card of a page that saves in its header. -->\n\t\t<div\n\t\t\tv-if=\"pageSave && changedFields.length > 0\"\n\t\t\tclass=\"cn-settings-page__changes\"\n\t\t\tdata-testid=\"cn-settings-page-changes\">\n\t\t\t<h2 class=\"cn-settings-page__changes-title\">\n\t\t\t\t{{ changesLabel }}\n\t\t\t</h2>\n\t\t\t<ul>\n\t\t\t\t<li v-for=\"field in changedFields\" :key=\"field.key\">\n\t\t\t\t\t{{ field.label }}\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\n\t\t<!-- Actions slot for save / discard / reset overrides -->\n\t\t<div v-if=\"$slots.actions || $slots.actions\" class=\"cn-settings-page__actions\">\n\t\t\t<slot name=\"actions\" />\n\t\t</div>\n\n\t\t<!-- Tab strip — only rendered when `tabs[]` is set\n\t\t\t (manifest-settings-orchestration REQ-MSO-5). Built with\n\t\t\t native <button role=\"tab\"> + ARIA wiring; uses Nextcloud\n\t\t\t CSS variables only (no hex literals). -->\n\t\t<div\n\t\t\tv-if=\"hasTabs\"\n\t\t\tclass=\"cn-settings-page__tabs\"\n\t\t\trole=\"tablist\"\n\t\t\tdata-testid=\"cn-settings-page-tabs\">\n\t\t\t<button\n\t\t\t\tv-for=\"(tab, tabIndex) in tabs\"\n\t\t\t\t:key=\"`tab-${tab.id}`\"\n\t\t\t\trole=\"tab\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-settings-page__tab\"\n\t\t\t\t:class=\"{ 'cn-settings-page__tab--active': activeTabId === tab.id }\"\n\t\t\t\t:aria-selected=\"activeTabId === tab.id ? 'true' : 'false'\"\n\t\t\t\t:aria-controls=\"`cn-settings-tab-panel-${tab.id}`\"\n\t\t\t\t:data-testid=\"`cn-settings-page-tab-${tab.id}`\"\n\t\t\t\t@click=\"onTabClick(tab, tabIndex)\">\n\t\t\t\t{{ resolveLabel(tab.label) }}\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- Sections — rendered for the active tab when `tabs[]` is\n\t\t\t set, otherwise the flat `sections[]` (back-compat). The wrapper\n\t\t\t draws no box of its own (`display: contents`); the board look\n\t\t\t turns it into the card grid. -->\n\t\t<div class=\"cn-settings-page__sections\">\n\t\t\t<CnSettingsCard\n\t\t\t\tv-for=\"(section, sectionIndex) in activeSections\"\n\t\t\t\t:key=\"`section-${activeTabId || 'flat'}-${sectionIndex}`\"\n\t\t\t\t:title=\"resolveLabel(section.title)\"\n\t\t\t\t:icon=\"section.icon || ''\"\n\t\t\t\t:collapsible=\"section.collapsible || false\">\n\t\t\t\t<!--\n\t\t\t\t\tBody resolution — see `sectionBodyKind(section)` in the\n\t\t\t\t\tscript: returns 'fields' (default + back-compat),\n\t\t\t\t\t'component', or 'widgets'.\n\t\t\t\t-->\n\n\t\t\t\t<!-- Body: bare fields[] (back-compat). Wrapped in a\n\t\t\t\t\t CnSettingsSection mirroring the pre-rich-sections layout. -->\n\t\t\t\t<CnSettingsSection\n\t\t\t\t\tv-if=\"sectionBodyKind(section) === 'fields'\"\n\t\t\t\t\t:name=\"resolveLabel(section.title)\"\n\t\t\t\t\t:description=\"resolveLabel(section.description)\"\n\t\t\t\t\t:docUrl=\"section.docUrl || ''\"\n\t\t\t\t\t:wide=\"Boolean(section.wide)\">\n\t\t\t\t\t<div class=\"cn-settings-page__fields\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tv-for=\"field in section.fields\"\n\t\t\t\t\t\t\t:key=\"field.key\"\n\t\t\t\t\t\t\tclass=\"cn-settings-page__field\">\n\t\t\t\t\t\t\t<!-- Per-field slot — `field-<key>` lets a consumer override the input entirely. -->\n\t\t\t\t\t\t\t<slot\n\t\t\t\t\t\t\t\t:name=\"`field-${field.key}`\"\n\t\t\t\t\t\t\t\t:field=\"field\"\n\t\t\t\t\t\t\t\t:value=\"formData[field.key]\"\n\t\t\t\t\t\t\t\t:onInput=\"(v) => updateField(field.key, v)\">\n\t\t\t\t\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t\t\t\t\tv-if=\"field.type === 'boolean'\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"!!formData[field.key]\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event)\">\n\t\t\t\t\t\t\t\t\t{{ resolveLabel(field.label) }}\n\t\t\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t\t\t<NcTextField\n\t\t\t\t\t\t\t\t\tv-else-if=\"field.type === 'number'\"\n\t\t\t\t\t\t\t\t\t:label=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"String(fieldValue(field.key, ''))\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event === '' ? null : Number($event))\" />\n\t\t\t\t\t\t\t\t<NcTextField\n\t\t\t\t\t\t\t\t\tv-else-if=\"field.type === 'password'\"\n\t\t\t\t\t\t\t\t\t:label=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"fieldValue(field.key, '')\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event)\" />\n\t\t\t\t\t\t\t\t<NcSelect\n\t\t\t\t\t\t\t\t\tv-else-if=\"field.type === 'enum' && Array.isArray(field.options)\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"selectedOption(field)\"\n\t\t\t\t\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t\t\t\t\t:inputLabel=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, optionValue($event))\" />\n\t\t\t\t\t\t\t\t<NcTextField\n\t\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\t\t:label=\"resolveLabel(field.label)\"\n\t\t\t\t\t\t\t\t\t:modelValue=\"fieldValue(field.key, '')\"\n\t\t\t\t\t\t\t\t\t@update:modelValue=\"updateField(field.key, $event)\" />\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t\t<small\n\t\t\t\t\t\t\t\tv-if=\"field.help\"\n\t\t\t\t\t\t\t\tclass=\"cn-settings-page__field-help\">\n\t\t\t\t\t\t\t\t{{ resolveLabel(field.help) }}\n\t\t\t\t\t\t\t</small>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<template v-if=\"sectionSave\" #footer>\n\t\t\t\t\t\t<div class=\"cn-settings-section__save\" data-testid=\"cn-settings-section-save\">\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\t\t\t\t\t@click=\"save\">\n\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t{{ saveLabel }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\t\t\t\t</CnSettingsSection>\n\n\t\t\t\t<!-- Body: a single registry-resolved component. The\n\t\t\t\t\t component is responsible for its own chrome — we do\n\t\t\t\t\t NOT wrap it in CnSettingsSection because the existing\n\t\t\t\t\t widget shape (CnVersionInfoCard, CnRegisterMapping)\n\t\t\t\t\t already wraps itself. Custom components are expected\n\t\t\t\t\t to do the same OR opt in by adding their own section\n\t\t\t\t\t wrapper.\n\n\t\t\t\t\t Wrapped in CnSettingsWidgetMount so the child's $emit\n\t\t\t\t\t is intercepted and re-emitted as @widget-event on this\n\t\t\t\t\t page (manifests can't carry inline JS). -->\n\t\t\t\t<CnSettingsWidgetMount\n\t\t\t\t\tv-else-if=\"sectionBodyKind(section) === 'component' && resolveSectionComponent(section)\"\n\t\t\t\t\t:component=\"resolveSectionComponent(section)\"\n\t\t\t\t\t:componentProps=\"section.props || {}\"\n\t\t\t\t\t:widgetType=\"section.component\"\n\t\t\t\t\t:sectionIndex=\"sectionIndex\"\n\t\t\t\t\t:widgetIndex=\"0\"\n\t\t\t\t\t@widgetEvent=\"onWidgetEvent\" />\n\n\t\t\t\t<!-- Body: ordered list of widgets. Each widget is its own\n\t\t\t\t\t mounted component with v-bind props + bubbled events\n\t\t\t\t\t via CnSettingsWidgetMount. Built-ins (version-info,\n\t\t\t\t\t register-mapping) wrap themselves in CnSettingsSection;\n\t\t\t\t\t custom widgets are expected to do the same. -->\n\t\t\t\t<template v-else-if=\"sectionBodyKind(section) === 'widgets'\">\n\t\t\t\t\t<CnSettingsWidgetMount\n\t\t\t\t\t\tv-for=\"entry in resolvedWidgetEntries(section, sectionIndex)\"\n\t\t\t\t\t\t:key=\"entry.key\"\n\t\t\t\t\t\t:component=\"entry.component\"\n\t\t\t\t\t\t:componentProps=\"entry.props\"\n\t\t\t\t\t\t:widgetType=\"entry.widgetType\"\n\t\t\t\t\t\t:sectionIndex=\"sectionIndex\"\n\t\t\t\t\t\t:widgetIndex=\"entry.widgetIndex\"\n\t\t\t\t\t\t@widgetEvent=\"onWidgetEvent\" />\n\t\t\t\t</template>\n\t\t\t</CnSettingsCard>\n\t\t</div>\n\n\t\t<!-- Save bar -->\n\t\t<div v-if=\"showSaveBar && !saveMode && !autosave\" class=\"cn-settings-page__save-bar\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"saving || !dirty\"\n\t\t\t\t@click=\"save\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"saving\" :size=\"20\" />\n\t\t\t\t\t<ContentSave v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"dirty\"\n\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t:disabled=\"saving\"\n\t\t\t\t@click=\"reset\">\n\t\t\t\t{{ resetLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Error -->\n\t\t<div v-if=\"lastError\" class=\"cn-settings-page__error\">\n\t\t\t{{ lastError }}\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport {\n\tNcButton,\n\tNcCheckboxRadioSwitch,\n\tNcLoadingIcon,\n\tNcSelect,\n\tNcTextField,\n} from '@nextcloud/vue'\nimport ContentSave from 'vue-material-design-icons/ContentSave.vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnRegisterMapping from '../CnRegisterMapping/CnRegisterMapping.vue'\nimport CnToggleTableWidget from '../CnToggleTableWidget/CnToggleTableWidget.vue'\nimport CnVersionInfoCard from '../CnVersionInfoCard/CnVersionInfoCard.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { CnPageHeader } from '../CnPageHeader/index.js'\nimport { CnSettingsCard } from '../CnSettingsCard/index.js'\nimport { CnSettingsSection } from '../CnSettingsSection/index.js'\nimport CnSettingsWidgetMount from './CnSettingsWidgetMount.js'\n\n/**\n * Sentinel value used in the built-in widget registry to mark the\n * `'component'` discriminator (manifest-settings-orchestration\n * REQ-MSO-6). The discriminator does NOT resolve to a fixed\n * component — instead, the resolver detects this sentinel and looks\n * up `widget.componentName` in the customComponents registry.\n */\nconst COMPONENT_DISCRIMINATOR = Symbol('cn-settings-component-widget')\n\n/**\n * Built-in widget registry. Used by `CnSettingsPage` to resolve\n * `widgets[].type` to a component BEFORE consulting the\n * consumer-provided customComponents registry.\n *\n * The order matters — built-ins win on collision so consumers can't\n * accidentally shadow `version-info` with their own component. If a\n * consumer needs to truly replace one of these, they can render their\n * own component via `section.component` or\n * `{ type: \"component\", componentName: <name> }` instead of `widgets[]`.\n *\n * Spec:\n * - REQ-MSRS-2 (manifest-settings-rich-sections) — fixed-component\n * built-ins (`version-info`, `register-mapping`).\n * - REQ-MSO-6 (manifest-settings-orchestration) — `'component'`\n * discriminator (sentinel value, resolved via componentName).\n */\nconst BUILTIN_SETTINGS_WIDGETS = Object.freeze({\n\t'version-info': CnVersionInfoCard,\n\t'register-mapping': CnRegisterMapping,\n\t// Rows with a switch: objects (`bind.mode: 'each'`) or a config map (`'map'`).\n\t'toggle-table': CnToggleTableWidget,\n\tcomponent: COMPONENT_DISCRIMINATOR,\n})\n\n/**\n * CnSettingsPage — Manifest-driven settings page.\n *\n * Reads `sections[]` from `pages[].config` and renders one\n * `CnSettingsCard` per section. Each section declares EXACTLY ONE of\n * three body kinds:\n *\n * - `fields[]` — flat form fields (back-compat default).\n * Each field's `type` controls which input renders:\n * - `boolean` → NcCheckboxRadioSwitch\n * - `number` → NcTextField (type=number)\n * - `string` → NcTextField (default)\n * - `password` → NcTextField (type=password)\n * - `enum` → NcSelect (requires `options: string[]`)\n * Per-field `#field-<key>` slots are exposed so consumers can drop\n * in their own widget.\n *\n * - `component` — registry-resolved component as the section body.\n * `section.props` is v-bind'd to it. Useful when a whole section\n * is one bespoke widget the library doesn't know about.\n *\n * - `widgets[]` — ordered list of widgets. Each entry has a `type`\n * string and optional `props`. Built-in widget types resolve\n * first (`version-info` → `CnVersionInfoCard`, `register-mapping`\n * → `CnRegisterMapping`, `toggle-table` → `CnToggleTableWidget`);\n * unknown types fall back to the\n * `customComponents` registry.\n *\n * Widget events (e.g. CnRegisterMapping's `@save`, CnVersionInfoCard's\n * `@update`) bubble up as a single `@widget-event` event on the page,\n * with payload `{ widgetType, widgetIndex, sectionIndex, name, args }`.\n * Consumers wire one handler at the CnAppRoot level and dispatch by\n * `widgetType` / `name` — the manifest can't carry inline JS, so this\n * is the documented event-wiring escape hatch.\n *\n * Saves via `axios.put(saveEndpoint, formData)`. The `saveEndpoint`\n * default is `/index.php/apps/{appId}/api/settings` — consumers should\n * pass a fully-qualified URL via the manifest (or `saveEndpoint` prop)\n * because the library has no knowledge of the consumer's app id at\n * import time.\n *\n * Slots:\n * - `#header` — Replaces the CnPageHeader.\n * - `#actions` — Right-aligned actions area (the renderer fills this\n * via `pages[].actionsComponent`).\n * - `#field-<key>` — Replaces the input for a specific field within\n * a `fields[]` section. Scope: `{ field, value, onInput }`.\n *\n * Events:\n * - `@save` — Emitted on successful save. Payload: the form data.\n * - `@error` — Emitted when save fails. Payload: the error.\n * - `@input` — Emitted on every field change. Payload: `{ key, value }`.\n * - `@widget-event` — Emitted when a widget mounted via `widgets[]`\n * or `component` re-emits one of its own events. Payload:\n * `{ widgetType, widgetIndex, sectionIndex, name, args }`.\n */\nexport default {\n\tname: 'CnSettingsPage',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcLoadingIcon,\n\t\tNcSelect,\n\t\tNcTextField,\n\t\tContentSave,\n\t\tCnBuildiqEditButton,\n\t\tCnSettingsCard,\n\t\tCnSettingsSection,\n\t\tCnPageHeader,\n\t\tCnSettingsWidgetMount,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Custom-component registry injected from CnAppRoot. Used to\n\t\t * resolve `section.component` and to fall back when a\n\t\t * `widgets[].type` isn't in the built-in widget map. Defaults\n\t\t * to an empty object when the page is mounted standalone.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The look the page is drawn in: `board` or `nextcloud`. Empty follows\n\t\t * the `cnLook` that CnAppRoot (or the page's `config.look`) provides.\n\t\t *\n\t\t * @spec openspec/changes/screens-chrome-parity/specs/app-look/spec.md#requirement-an-app-can-take-the-board-look\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Where the page saves (`config.saveMode`): `section` ends each section\n\t\t * that has fields in its own Save button, `page` draws one Save as the\n\t\t * last header button and a Changes card, and empty keeps the save bar\n\t\t * under the last section. Never both a section and a page save.\n\t\t *\n\t\t * @spec openspec/changes/screens-chrome-parity/specs/settings-board-look/spec.md#requirement-a-settings-page-saves-in-one-declared-place\n\t\t * @type {'section'|'page'|''}\n\t\t */\n\t\tsaveMode: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (value) => ['', 'section', 'page'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * The page saves on change (`config.autosave`): no save button at all,\n\t\t * and the description ends with \"Changes are saved automatically.\"\n\t\t *\n\t\t * @spec openspec/changes/screens-chrome-parity/specs/settings-board-look/spec.md#requirement-a-settings-page-saves-in-one-declared-place\n\t\t */\n\t\tautosave: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Page title. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Settings'),\n\t\t},\n\n\t\t/**\n\t\t * A breadcrumb under the title (manifest `config.breadcrumb`): one\n\t\t * crumb `{ label, route?, params?, href?, icon?, separator? }` or a\n\t\t * list; the title follows as the current crumb. Drawn as the detail\n\t\t * header's breadcrumb row. Null (the default) draws none.\n\t\t *\n\t\t * @type {object|Array<object>|null}\n\t\t * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note\n\t\t */\n\t\tbreadcrumb: {\n\t\t\ttype: [Object, Array],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A note after the breadcrumb and a middle dot (manifest `config.headerMeta`).\n\t\t *\n\t\t * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note\n\t\t */\n\t\theaderMeta: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Description shown under the title when `showTitle` is set. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether to render the inline page header VISIBLY. When false the\n\t\t * `<h1>` is still rendered visually-hidden, so the `<main>` landmark\n\t\t * always has an accessible heading (WCAG 2.4.6 / 1.3.1).\n\t\t */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** MDI icon name for the header. */\n\t\ticon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Section definitions (flat shape — back-compat). Each section\n\t\t * MUST declare EXACTLY ONE of:\n\t\t * - `fields: Array<Field>` (back-compat flat-field body)\n\t\t * - `component: <registry-name>` + optional `props`\n\t\t * - `widgets: Array<{ type, props?, componentName? }>`\n\t\t *\n\t\t * Common keys: `{ title, description?, icon?, collapsible?, docUrl? }`.\n\t\t *\n\t\t * Mutually exclusive with `tabs[]` (XOR — see\n\t\t * manifest-settings-orchestration REQ-MSO-1).\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Tab definitions (orchestration shape — manifest-settings-\n\t\t * orchestration REQ-MSO-2). When set, CnSettingsPage renders\n\t\t * a tab strip above the section area; the active tab's\n\t\t * `sections[]` flow into the same renderer used by the flat\n\t\t * shape. Mutually exclusive with `sections[]`.\n\t\t *\n\t\t * Each tab MUST be `{ id: string, label: string,\n\t\t * icon?: string, sections: array<Section> }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\ttabs: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Optional ID of the tab to activate on mount. When empty AND\n\t\t * `tabs[]` is non-empty, the first tab is active by default.\n\t\t * Unknown IDs fall back to the first tab.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tinitialTab: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Initial values keyed by `field.key`. Defaults to an empty\n\t\t * object; in practice the consumer passes the current\n\t\t * IAppConfig snapshot loaded from their settings controller.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tinitialValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Endpoint that receives the PUT on save. Pass a fully-qualified\n\t\t * URL — the library has no knowledge of the consumer's app id.\n\t\t * When empty, no PUT is issued and `@save` is emitted with the\n\t\t * form data so the consumer can persist it themselves.\n\t\t */\n\t\tsaveEndpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether to render the built-in save/reset bar. */\n\t\tshowSaveBar: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Label for the save button. */\n\t\tsaveLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Save'),\n\t\t},\n\n\t\t/** Label for the reset (discard) button. */\n\t\tresetLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Discard changes'),\n\t\t},\n\n\t\t/**\n\t\t * Optional translation function. When provided, applied to\n\t\t * section titles, field labels, and other i18n-key strings\n\t\t * declared in the manifest. Defaults to the identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit custom-component registry. When set, takes\n\t\t * precedence over the injected `cnCustomComponents`. Use this\n\t\t * when mounting CnSettingsPage outside a CnAppRoot tree (or\n\t\t * when test-mounting with a stub registry).\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcustomComponents: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['save', 'error', 'input', 'widget-event', 'tab-change'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\t// Resolve the initial active-tab id synchronously so the very\n\t\t// first render has the correct tab active (otherwise tests\n\t\t// that mount + assert without an `await tick` see the empty\n\t\t// default). Mirrors the logic in `resolveInitialTabId` (the\n\t\t// watcher path); keep them aligned.\n\t\tlet activeTabId = ''\n\t\tconst tabs = Array.isArray(this.tabs) ? this.tabs : []\n\t\tif (tabs.length > 0) {\n\t\t\tif (typeof this.initialTab === 'string' && this.initialTab.length > 0\n\t\t\t\t&& tabs.some((t) => t && t.id === this.initialTab)) {\n\t\t\t\tactiveTabId = this.initialTab\n\t\t\t} else if (tabs[0] && typeof tabs[0].id === 'string') {\n\t\t\t\tactiveTabId = tabs[0].id\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tformData: this.cloneInitial(),\n\t\t\toriginalData: this.cloneInitial(),\n\t\t\tsaving: false,\n\t\t\tlastError: null,\n\t\t\tactiveTabId,\n\t\t\tautosaveTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} The page saves from its header (`saveMode: \"page\"`, not autosave). */\n\t\tpageSave() {\n\t\t\treturn this.saveMode === 'page' && !this.autosave\n\t\t},\n\n\t\t/** @return {boolean} Each section with fields saves on its own (`saveMode: \"section\"`). */\n\t\tsectionSave() {\n\t\t\treturn this.saveMode === 'section' && !this.autosave\n\t\t},\n\n\t\t/**\n\t\t * The description, with the autosave note appended when the page saves\n\t\t * on change.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedDescription() {\n\t\t\tif (!this.autosave) {\n\t\t\t\treturn this.description\n\t\t\t}\n\t\t\tconst note = t('nextcloud-vue', 'Changes are saved automatically.')\n\t\t\treturn this.description ? `${this.description} ${note}` : note\n\t\t},\n\n\t\t/** @return {boolean} The header buttons render inside the visible page header. */\n\t\theaderButtonsInHeader() {\n\t\t\treturn Boolean(this.title && this.showTitle && !this.$slots.header && (this.isBoard || this.pageSave))\n\t\t},\n\n\t\t/** @return {boolean} The header buttons need a row of their own (header hidden or replaced). */\n\t\theaderButtonsOutside() {\n\t\t\treturn Boolean((this.isBoard || this.pageSave) && !this.headerButtonsInHeader)\n\t\t},\n\n\t\t/** @return {string} Title of the Changes card. */\n\t\tchangesLabel() {\n\t\t\treturn t('nextcloud-vue', 'Changes')\n\t\t},\n\n\t\t/**\n\t\t * The fields that differ from their saved value, across every section,\n\t\t * labelled for the Changes card.\n\t\t *\n\t\t * @return {Array<{key: string, label: string}>}\n\t\t */\n\t\tchangedFields() {\n\t\t\tconst labels = {}\n\t\t\tconst sections = [...(this.sections || [])]\n\t\t\tfor (const tab of this.tabs || []) {\n\t\t\t\tif (tab && Array.isArray(tab.sections)) {\n\t\t\t\t\tsections.push(...tab.sections)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const section of sections) {\n\t\t\t\tfor (const field of (section && section.fields) || []) {\n\t\t\t\t\tlabels[field.key] = this.resolveLabel(field.label) || field.key\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst keys = new Set([...Object.keys(this.formData), ...Object.keys(this.originalData)])\n\t\t\treturn [...keys]\n\t\t\t\t.filter((key) => JSON.stringify(this.formData[key]) !== JSON.stringify(this.originalData[key]))\n\t\t\t\t.map((key) => ({ key, label: labels[key] || key }))\n\t\t},\n\n\t\t/** Whether any field has changed since load. */\n\t\tdirty() {\n\t\t\treturn JSON.stringify(this.formData) !== JSON.stringify(this.originalData)\n\t\t},\n\n\t\t/**\n\t\t * Effective custom-component registry. Explicit prop wins over\n\t\t * the injected value (mirrors CnPageRenderer's resolution\n\t\t * order).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveCustomComponents() {\n\t\t\treturn this.customComponents ?? this.cnCustomComponents ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the page is in tabs orchestration mode. True when\n\t\t * `tabs[]` is non-empty — drives the tab-strip render gate\n\t\t * (manifest-settings-orchestration REQ-MSO-5).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasTabs() {\n\t\t\treturn Array.isArray(this.tabs) && this.tabs.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The sections to render right now. In flat mode, this is the\n\t\t * `sections` prop directly. In tabs mode, this is the\n\t\t * `sections[]` array of the currently active tab. Centralising\n\t\t * this in one computed keeps the template's `v-for` simple\n\t\t * and decouples it from the body kind dispatcher (which\n\t\t * applies per-section, not per-mode).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tactiveSections() {\n\t\t\tif (!this.hasTabs) {\n\t\t\t\treturn this.sections || []\n\t\t\t}\n\t\t\tconst active = this.tabs.find((t) => t && t.id === this.activeTabId)\n\t\t\tif (active && Array.isArray(active.sections)) {\n\t\t\t\treturn active.sections\n\t\t\t}\n\t\t\t// Defensive fallback — should not happen because\n\t\t\t// `resolveInitialTabId` always lands on a known tab.\n\t\t\tconst first = this.tabs[0]\n\t\t\treturn first && Array.isArray(first.sections) ? first.sections : []\n\t\t},\n\t},\n\n\twatch: {\n\t\tinitialValues: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.formData = this.cloneInitial()\n\t\t\t\tthis.originalData = this.cloneInitial()\n\t\t\t},\n\t\t},\n\n\t\t// When `tabs[]` changes (e.g. consumer swaps manifests at\n\t\t// runtime), re-resolve the active tab so the page doesn't get\n\t\t// stuck on a removed id.\n\t\ttabs: {\n\t\t\thandler() {\n\t\t\t\tthis.activeTabId = this.resolveInitialTabId()\n\t\t\t},\n\t\t},\n\n\t\t// When `initialTab` changes (consumer-controlled tab\n\t\t// activation), follow it.\n\t\tinitialTab(next) {\n\t\t\tif (typeof next === 'string' && next.length > 0) {\n\t\t\t\tconst exists = this.tabs.some((t) => t && t.id === next)\n\t\t\t\tif (exists) {\n\t\t\t\t\tthis.activeTabId = next\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tclearTimeout(this.autosaveTimer)\n\t},\n\n\tmethods: {\n\t\t// Coalesce a field value to a fallback when null / undefined.\n\t\t// Used in template bindings instead of `formData[key] ?? ''` because\n\t\t// vue-template-es2015-compiler (the Rollup VuePlugin's parser) does\n\t\t// not accept ES2020 nullish-coalescing inside template expressions.\n\t\tfieldValue(key, fallback) {\n\t\t\tconst v = this.formData[key]\n\t\t\treturn (v === null || v === undefined) ? fallback : v\n\t\t},\n\n\t\t// Resolve the currently-selected option for an enum field.\n\t\t// `field.options` may be an array of strings/numbers OR an array\n\t\t// of `{ label, value }`-shaped objects. NcSelect needs the actual\n\t\t// option entry (not the bare value) to render the selection\n\t\t// correctly, so this resolver looks up the matching object form\n\t\t// when a primitive value is stored. Returns `null` when nothing\n\t\t// is selected so NcSelect renders the empty state instead of\n\t\t// emitting a Vue warning about an undefined `value` prop.\n\t\tselectedOption(field) {\n\t\t\tconst v = this.formData[field.key]\n\t\t\tif (v === null || v === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (!Array.isArray(field.options)) {\n\t\t\t\treturn v\n\t\t\t}\n\t\t\tfor (const opt of field.options) {\n\t\t\t\tif (opt && typeof opt === 'object') {\n\t\t\t\t\tif (opt.value === v) {\n\t\t\t\t\t\treturn opt\n\t\t\t\t\t}\n\t\t\t\t} else if (opt === v) {\n\t\t\t\t\treturn opt\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn v\n\t\t},\n\n\t\t// Inverse of `selectedOption` — extract the storable value from\n\t\t// whatever NcSelect emits. NcSelect emits the full option entry\n\t\t// when options are objects, the primitive when options are\n\t\t// primitives, or `null` on clear.\n\t\toptionValue(emitted) {\n\t\t\tif (emitted === null || emitted === undefined) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof emitted === 'object' && 'value' in emitted) {\n\t\t\t\treturn emitted.value\n\t\t\t}\n\t\t\treturn emitted\n\t\t},\n\n\t\tcloneInitial() {\n\t\t\tconst merged = { ...(this.initialValues || {}) }\n\t\t\t// Collect every section across both modes (flat\n\t\t\t// `sections[]` AND `tabs[].sections[]`) so default values\n\t\t\t// are applied regardless of orchestration shape.\n\t\t\t// Only flat-field sections contribute defaults; component\n\t\t\t// and widgets sections own their own state.\n\t\t\tconst allSections = []\n\t\t\tfor (const section of this.sections || []) {\n\t\t\t\tallSections.push(section)\n\t\t\t}\n\t\t\tfor (const tab of this.tabs || []) {\n\t\t\t\tif (tab && Array.isArray(tab.sections)) {\n\t\t\t\t\tfor (const section of tab.sections) {\n\t\t\t\t\t\tallSections.push(section)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const section of allSections) {\n\t\t\t\tif (!section || !Array.isArray(section.fields)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor (const field of section.fields) {\n\t\t\t\t\tif (field.default !== undefined && merged[field.key] === undefined) {\n\t\t\t\t\t\tmerged[field.key] = field.default\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn merged\n\t\t},\n\n\t\tresolveLabel(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (this.translate) {\n\t\t\t\treturn this.translate(value)\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\tupdateField(key, value) {\n\t\t\tthis.formData[key] = value\n\t\t\tthis.$emit('input', { key, value })\n\t\t\tif (this.autosave) {\n\t\t\t\tthis.queueAutosave()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Determine which body kind a section declares. Returns\n\t\t * `'fields'` (default + back-compat), `'component'`, or\n\t\t * `'widgets'`. The validator (`validateManifest`) enforces\n\t\t * exactly-one at parse time; this method is the runtime\n\t\t * equivalent and is permissive when one of the keys is set\n\t\t * (last-defined wins in the rare case of an unvalidated\n\t\t * manifest).\n\t\t *\n\t\t * @param {object} section A section entry from `sections[]`.\n\t\t * @return {'fields' | 'component' | 'widgets'}\n\t\t */\n\t\tsectionBodyKind(section) {\n\t\t\tif (Array.isArray(section.widgets) && section.widgets.length > 0) {\n\t\t\t\treturn 'widgets'\n\t\t\t}\n\t\t\tif (typeof section.component === 'string' && section.component.length > 0) {\n\t\t\t\treturn 'component'\n\t\t\t}\n\t\t\treturn 'fields'\n\t\t},\n\n\t\t/**\n\t\t * Resolve a `section.component` registry name to a Vue\n\t\t * component via `effectiveCustomComponents`. Returns `null`\n\t\t * (and warns) when the registry has no entry.\n\t\t *\n\t\t * @param {object} section A section entry.\n\t\t * @return {object|null} Vue component or null.\n\t\t */\n\t\tresolveSectionComponent(section) {\n\t\t\tconst name = section.component\n\t\t\tif (!name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = this.effectiveCustomComponents[name]\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnSettingsPage] Section component \"${name}\" not found in customComponents registry. Section body will be empty.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/**\n\t\t * Resolve a single `widgets[]` entry to a concrete Vue\n\t\t * component. Lookup order:\n\t\t *\n\t\t * 1. Built-in widget map (`version-info`, `register-mapping`).\n\t\t * 2. `'component'` discriminator (REQ-MSO-6) — resolves\n\t\t * `widget.componentName` against `effectiveCustomComponents`.\n\t\t * 3. Legacy fallback — looks up `widget.type` against\n\t\t * `effectiveCustomComponents`. Kept for back-compat with\n\t\t * manifest-settings-rich-sections consumers; flagged as\n\t\t * deprecated in JSDoc — manifest authors should migrate to\n\t\t * the explicit `{ type: \"component\", componentName }` shape.\n\t\t *\n\t\t * Returns `null` (and warns) when nothing resolves. Built-ins\n\t\t * win on collision so consumers can't accidentally shadow them.\n\t\t *\n\t\t * @param {object} widget A `widgets[]` entry, e.g. `{ type, props?, componentName? }`.\n\t\t * @return {object|null} Vue component or null.\n\t\t */\n\t\tresolveWidgetComponent(widget) {\n\t\t\tconst type = widget && typeof widget.type === 'string' ? widget.type : ''\n\t\t\tif (!type) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (Object.hasOwn(BUILTIN_SETTINGS_WIDGETS, type)) {\n\t\t\t\tconst builtin = BUILTIN_SETTINGS_WIDGETS[type]\n\t\t\t\tif (builtin === COMPONENT_DISCRIMINATOR) {\n\t\t\t\t\t// REQ-MSO-6: discriminator — look up `componentName`.\n\t\t\t\t\tconst name = widget.componentName\n\t\t\t\t\tif (typeof name !== 'string' || name.length === 0) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn('[CnSettingsPage] Widget {type:\"component\"} requires a non-empty `componentName`. Widget will be skipped.')\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\tconst resolved = this.effectiveCustomComponents[name]\n\t\t\t\t\tif (!resolved) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`[CnSettingsPage] Widget component \"${name}\" not found in customComponents registry. Widget will be skipped.`)\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\treturn resolved\n\t\t\t\t}\n\t\t\t\treturn builtin\n\t\t\t}\n\t\t\t// Legacy fallback (manifest-settings-rich-sections REQ-MSRS-2).\n\t\t\t// Deprecated — manifest authors should migrate to\n\t\t\t// `{ type: \"component\", componentName: <X> }`. Kept here so\n\t\t\t// existing consumers continue working unchanged.\n\t\t\tconst resolved = this.effectiveCustomComponents[type]\n\t\t\tif (!resolved) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnSettingsPage] Widget type \"${type}\" not found in built-in widgets or customComponents registry. Widget will be skipped.`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/**\n\t\t * Build the list of widget mount entries for a `widgets[]`\n\t\t * section. Filters out entries whose `widget.type` doesn't\n\t\t * resolve (built-in OR customComponents) — `resolveWidgetComponent`\n\t\t * has already logged a warn. The filter happens here so the\n\t\t * template can use a clean `v-for` without nested `v-if`.\n\t\t *\n\t\t * The `widgetType` carried on the bubbled `@widget-event`\n\t\t * payload is the widget's `componentName` (when the\n\t\t * discriminator is `'component'`) or `widget.type` otherwise —\n\t\t * giving consumers a stable identifier for the dispatch.\n\t\t *\n\t\t * @param {object} section A section entry with `widgets[]`.\n\t\t * @param {number} sectionIndex Index in `sections[]`.\n\t\t * @return {Array<{key: string, component: object, props: object, widgetType: string, widgetIndex: number}>}\n\t\t */\n\t\tresolvedWidgetEntries(section, sectionIndex) {\n\t\t\tconst entries = []\n\t\t\tconst widgets = Array.isArray(section.widgets) ? section.widgets : []\n\t\t\tfor (let widgetIndex = 0; widgetIndex < widgets.length; widgetIndex++) {\n\t\t\t\tconst widget = widgets[widgetIndex] || {}\n\t\t\t\tconst component = this.resolveWidgetComponent(widget)\n\t\t\t\tif (!component) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst widgetType = widget.type === 'component' && typeof widget.componentName === 'string'\n\t\t\t\t\t? widget.componentName\n\t\t\t\t\t: widget.type\n\t\t\t\tentries.push({\n\t\t\t\t\tkey: `widget-${sectionIndex}-${widgetIndex}`,\n\t\t\t\t\tcomponent,\n\t\t\t\t\tprops: widget.props || {},\n\t\t\t\t\twidgetType,\n\t\t\t\t\twidgetIndex,\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn entries\n\t\t},\n\n\t\t/**\n\t\t * Resolve the active-tab id on mount / when `tabs[]` changes.\n\t\t * Lookup order: explicit `initialTab` prop → first tab in\n\t\t * `tabs[]` → empty string. Unknown `initialTab` values fall\n\t\t * back to the first tab so the page never gets stuck.\n\t\t * (manifest-settings-orchestration REQ-MSO-5.)\n\t\t *\n\t\t * @return {string} The resolved tab id (empty in flat mode).\n\t\t */\n\t\tresolveInitialTabId() {\n\t\t\tif (!this.hasTabs) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (typeof this.initialTab === 'string' && this.initialTab.length > 0) {\n\t\t\t\tconst exists = this.tabs.some((t) => t && t.id === this.initialTab)\n\t\t\t\tif (exists) {\n\t\t\t\t\treturn this.initialTab\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst first = this.tabs[0]\n\t\t\treturn first && typeof first.id === 'string' ? first.id : ''\n\t\t},\n\n\t\t/**\n\t\t * Handle a tab button click. Switches the active tab and\n\t\t * emits `@tab-change` so consumers can react (e.g. persist the\n\t\t * active tab in their preference store, update the URL hash).\n\t\t * (manifest-settings-orchestration REQ-MSO-5.)\n\t\t *\n\t\t * @param {object} tab The clicked tab definition.\n\t\t * @param {number} tabIndex The tab's index in `tabs[]`.\n\t\t */\n\t\tonTabClick(tab, tabIndex) {\n\t\t\tif (!tab || typeof tab.id !== 'string') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.activeTabId === tab.id) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.activeTabId = tab.id\n\t\t\tthis.$emit('tab-change', { tabId: tab.id, tabIndex })\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a widget's `widget-event` (caught by the local\n\t\t * CnSettingsWidgetMount helper) as a top-level `widget-event`\n\t\t * on this page. Consumers wire one `@widget-event` handler at\n\t\t * the CnAppRoot mount point and dispatch by `widgetType` /\n\t\t * `name`.\n\t\t *\n\t\t * @param {object} payload `{ widgetType, widgetIndex, sectionIndex, name, args }`\n\t\t */\n\t\tonWidgetEvent(payload) {\n\t\t\tthis.$emit('widget-event', payload)\n\t\t},\n\n\t\t/**\n\t\t * Save shortly after the last change (`autosave`): a burst of typing\n\t\t * is one request.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tqueueAutosave() {\n\t\t\tclearTimeout(this.autosaveTimer)\n\t\t\tthis.autosaveTimer = setTimeout(() => {\n\t\t\t\tthis.autosaveTimer = null\n\t\t\t\tif (this.dirty && !this.saving) {\n\t\t\t\t\tthis.save()\n\t\t\t\t}\n\t\t\t}, 600)\n\t\t},\n\n\t\treset() {\n\t\t\tthis.formData = this.cloneInitial()\n\t\t\tthis.lastError = null\n\t\t},\n\n\t\tasync save() {\n\t\t\tthis.saving = true\n\t\t\tthis.lastError = null\n\t\t\ttry {\n\t\t\t\tif (this.saveEndpoint) {\n\t\t\t\t\tawait axios.put(prefixUrl(this.saveEndpoint), this.formData)\n\t\t\t\t}\n\t\t\t\tthis.originalData = JSON.parse(JSON.stringify(this.formData))\n\t\t\t\tthis.$emit('save', this.formData)\n\t\t\t} catch (err) {\n\t\t\t\tthis.lastError = err?.response?.data?.message || err?.message || t('nextcloud-vue', 'Save failed')\n\t\t\t\tthis.$emit('error', err)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The section wrapper draws no box; the board look makes it the card grid. */\n.cn-settings-page__sections {\n\tdisplay: contents;\n}\n\n.cn-settings-page__header-buttons {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmargin-inline-start: auto;\n}\n\n.cn-settings-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.cn-settings-page__actions {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n}\n\n/*\n * Tab strip for the orchestration shape (manifest-settings-\n * orchestration REQ-MSO-5). Uses Nextcloud CSS variables only — no\n * hex literals, no rgba overrides on the elements themselves.\n */\n.cn-settings-page__tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px;\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding-bottom: 0;\n\tmargin-bottom: 8px;\n}\n\n.cn-settings-page__tab {\n\tbackground: transparent;\n\tborder: 0;\n\tborder-bottom: 3px solid transparent;\n\tpadding: 8px 16px;\n\tmargin: 0;\n\tcursor: pointer;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: normal;\n\tborder-radius: var(--border-radius-large) var(--border-radius-large) 0 0;\n\ttransition: background-color 0.1s ease-in-out, color 0.1s ease-in-out, border-color 0.1s ease-in-out;\n}\n\n.cn-settings-page__tab:hover,\n.cn-settings-page__tab:focus-visible {\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n\toutline: none;\n}\n\n.cn-settings-page__tab--active {\n\tcolor: var(--color-primary-element);\n\tborder-bottom-color: var(--color-primary-element);\n\tfont-weight: bold;\n}\n\n@media (max-width: 768px) {\n\t.cn-settings-page__tabs {\n\t\tflex-direction: column;\n\t\tgap: 0;\n\t\tborder-bottom: 0;\n\t}\n\n\t.cn-settings-page__tab {\n\t\tborder-bottom: 1px solid var(--color-border);\n\t\tborder-radius: 0;\n\t\ttext-align: left;\n\t}\n\n\t.cn-settings-page__tab--active {\n\t\tborder-bottom-width: 1px;\n\t\tbackground-color: var(--color-background-hover);\n\t}\n}\n\n.cn-settings-page__fields {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-settings-page__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-settings-page__field-help {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-page__save-bar {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-top: 16px;\n}\n\n.cn-settings-page__error {\n\tcolor: var(--color-error);\n\tbackground: var(--color-error-light, var(--color-background-hover));\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n}\n</style>\n"],"names":["_renderSlot","_createBlock","_createElementVNode","_createTextVNode","_openBlock","_createElementBlock","_toDisplayString","_Fragment","_renderList","_createVNode"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnTabs.vue2.js';
2
2
  import { render } from './CnTabs.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-019bacbf";
5
+ script.__scopeId = "data-v-4d96de3d";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -76,6 +76,21 @@ var script = defineComponent({
76
76
  default: '',
77
77
  },
78
78
 
79
+ /**
80
+ * Leave the `overflow` tabs to the host's `#nav-end` menu instead of a
81
+ * "More" menu of the strip's own. The strip then draws only the tabs
82
+ * that are not `overflow`, and an overflow tab that is selected joins
83
+ * it just before the last of them (board DqZaakBesluiten: Overview,
84
+ * Files, Contact, Tasks, Decisions, History). The host lists the
85
+ * overflow tabs and selects one through its `CnTab`'s `active`.
86
+ *
87
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-board-tabs-list-their-overflow-in-the-actions-menu
88
+ */
89
+ overflowInNavEnd: {
90
+ type: Boolean,
91
+ default: false,
92
+ },
93
+
79
94
  /** Name of the menu that lists the `overflow` tabs. */
80
95
  moreLabel: {
81
96
  type: String,
@@ -190,14 +205,36 @@ var script = defineComponent({
190
205
  *
191
206
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
192
207
  */
193
- const visibleTabs = computed(() => tabs.filter((tab) => boardStrip.value || !tab.overflow || tab.uid === activeUid.value));
208
+ const visibleTabs = computed(() => {
209
+ if (props.overflowInNavEnd) {
210
+ return navEndStrip()
211
+ }
212
+ return tabs.filter((tab) => boardStrip.value || !tab.overflow || tab.uid === activeUid.value)
213
+ });
214
+
215
+ /**
216
+ * The strip under `overflowInNavEnd`: the tabs that are not
217
+ * `overflow`, with a selected overflow tab placed before the last.
218
+ *
219
+ * @return {object[]} The tabs to draw, in order.
220
+ * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-an-opened-overflow-tab-joins-the-strip-before-the-last-tab
221
+ */
222
+ function navEndStrip() {
223
+ const pinned = tabs.filter((tab) => !tab.overflow);
224
+ const opened = tabs.find((tab) => tab.overflow && tab.uid === activeUid.value);
225
+ if (!opened) {
226
+ return pinned
227
+ }
228
+ const at = Math.max(0, pinned.length - 1);
229
+ return [...pinned.slice(0, at), opened, ...pinned.slice(at)]
230
+ }
194
231
 
195
232
  /**
196
233
  * The tabs listed under "More": overflow tabs that are not selected.
197
234
  *
198
235
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
199
236
  */
200
- const overflowTabs = computed(() => (boardStrip.value ? [] : tabs.filter((tab) => tab.overflow && tab.uid !== activeUid.value)));
237
+ const overflowTabs = computed(() => (boardStrip.value || props.overflowInNavEnd ? [] : tabs.filter((tab) => tab.overflow && tab.uid !== activeUid.value)));
201
238
 
202
239
  /**
203
240
  * Whether a tab declares a count. `0` is a count; null and '' are not.
@@ -1 +1 @@
1
- {"version":3,"file":"CnTabs.vue2.js","sources":["../../../../src/components/CnTabs/CnTabs.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-tabs\" :class=\"[{ 'cn-tabs--card': card, 'cn-tabs--segmented': variant === 'segmented', 'cn-tabs--underline': variant === 'underline', 'cn-tabs--board': boardStrip }, boardClass]\">\n\t\t<div class=\"cn-tabs__bar\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-tabs__strip\"\n\t\t\t\t:class=\"{ 'cn-tabs__strip--more-start': moreStart, 'cn-tabs__strip--more-end': moreEnd }\">\n\t\t\t\t<div\n\t\t\t\t\tref=\"nav\"\n\t\t\t\t\tclass=\"cn-tabs__nav\"\n\t\t\t\t\t:class=\"{ 'cn-tabs__nav--justified': justified }\"\n\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t:aria-label=\"ariaLabel || null\"\n\t\t\t\t\t@keydown=\"onNavKeydown\"\n\t\t\t\t\t@wheel=\"onNavWheel\"\n\t\t\t\t\t@scroll=\"measureOverflow\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"tab in visibleTabs\"\n\t\t\t\t\t\t:id=\"tab.tabId\"\n\t\t\t\t\t\t:key=\"tab.uid\"\n\t\t\t\t\t\tref=\"navButtons\"\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\tclass=\"cn-tabs__nav-item\"\n\t\t\t\t\t\t:class=\"{ 'cn-tabs__nav-item--active': isActive(tab.uid) }\"\n\t\t\t\t\t\t:aria-selected=\"isActive(tab.uid) ? 'true' : 'false'\"\n\t\t\t\t\t\t:aria-controls=\"tab.panelId\"\n\t\t\t\t\t\t:tabindex=\"isActive(tab.uid) ? 0 : -1\"\n\t\t\t\t\t\t:disabled=\"tab.disabled || null\"\n\t\t\t\t\t\t@click=\"tab.onActivate()\">\n\t\t\t\t\t\t<!-- The space before the count is load-bearing: it keeps\n\t\t\t\t\t\t the tab's accessible name \"Documents 5\" rather than\n\t\t\t\t\t\t \"Documents5\". The margin separates the two for the eye\n\t\t\t\t\t\t only, so keep both tags on one line. -->\n\t\t\t\t\t\t<component :is=\"tab.titleRender\" /> <span\n\t\t\t\t\t\t\tv-if=\"hasCount(tab)\"\n\t\t\t\t\t\t\tclass=\"cn-tabs__count\"\n\t\t\t\t\t\t\tdata-testid=\"cn-tabs-count\">{{ tab.count }}</span>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t\t<!-- Mouse-only affordances: keyboard users already have the arrow\n\t\t\t\t keys, and the tabs themselves stay in the accessibility tree. -->\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreStart\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--start\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(-1)\">\n\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreEnd\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--end\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(1)\">\n\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<!-- The tabs that did not make the strip. OUTSIDE the tablist, like\n\t\t\t `#nav-end`: a menu button nested in it would be announced as\n\t\t\t one more tab. A tab picked here joins the strip while it is\n\t\t\t selected, so the open panel always has a real tab to be\n\t\t\t labelled by. -->\n\t\t\t<div v-if=\"overflowTabs.length > 0\" class=\"cn-tabs__more\">\n\t\t\t\t<NcActions\n\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t:forceName=\"true\"\n\t\t\t\t\t:menuName=\"moreLabel\"\n\t\t\t\t\t:aria-label=\"moreAriaLabel\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\tdata-testid=\"cn-tabs-more\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ChevronDown :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\tv-for=\"tab in overflowTabs\"\n\t\t\t\t\t\t:key=\"tab.uid\"\n\t\t\t\t\t\t:disabled=\"tab.disabled || null\"\n\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-tabs-more-item\"\n\t\t\t\t\t\t@click=\"onOverflowPick(tab)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<span class=\"cn-tabs__more-spacer\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ overflowLabel(tab) }}\n\t\t\t\t\t</NcActionButton>\n\t\t\t\t</NcActions>\n\t\t\t</div>\n\t\t\t<div v-if=\"$slots['nav-end']\" class=\"cn-tabs__nav-end\">\n\t\t\t\t<!-- @slot nav-end Rendered at the right-hand end of the tab bar, deliberately OUTSIDE the `role=\"tablist\"` element. A widget Actions menu belongs beside the strip, not inside it: anything nested in the tablist is announced as one of the tabs, so a screen-reader user counting six tabs would hear seven. -->\n\t\t\t\t<!-- @binding {number} active-index Index of the currently selected tab. -->\n\t\t\t\t<slot name=\"nav-end\" :activeIndex=\"activeIndex\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"cn-tabs__content\" :class=\"contentClass\">\n\t\t\t<!-- @slot The CnTab children. Anything else is rendered into the panel area untouched. -->\n\t\t\t<slot />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * CnTabs — a generic tab strip.\n *\n * Neither `@nextcloud/vue@9` nor this library previously shipped one: the only\n * tab component in either is `NcAppSidebarTab`, which is meaningless outside an\n * `NcAppSidebar`. Apps that needed a plain tab strip in a page, a dialog or a\n * detail view were on `bootstrap-vue@2` — a Vue-2-only package with no Vue 3\n * release (`bootstrap-vue-next` is a different package with a different API) —\n * and pulling a second UI framework in alongside `@nextcloud/vue` cuts against\n * the fleet rule that apps render with Nextcloud components. So zaakafhandelapp\n * wrote a local `Tabs`/`Tab` pair for eight views and opencatalogi and\n * openregister flagged the same gap. This is that pair, lifted and hardened.\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\" justified>\n * <CnTab title=\"Documents\">…</CnTab>\n * <CnTab title=\"Tasks\" :active=\"showTasks\" @click=\"onTasks\">…</CnTab>\n * <CnTab>\n * <template #title><CnIcon name=\"close\" /> Draft</template>\n * …\n * </CnTab>\n * </CnTabs>\n * ```\n *\n * ## Putting controls beside the strip\n *\n * `#nav-end` fills the right-hand end of the tab bar. `CnTabsWidget` uses it to\n * carry the active child's Actions menu, so one menu serves every tab instead\n * of each panel drawing its own header:\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\">\n * <template #nav-end><CnActionsMenu :title=\"activeTitle\" /></template>\n * <CnTab title=\"Notes\">…</CnTab>\n * </CnTabs>\n * ```\n *\n * The slot renders OUTSIDE the `role=\"tablist\"` element on purpose. See the\n * comment on it in the template.\n *\n * ## Migrating from `bootstrap-vue`\n *\n * `<BTabs>`/`<BTab>` map across directly for the subset apps actually used:\n * `content-class`, `justified` and `card` on the strip; `title`, the `#title`\n * slot, `active` and `@click` on a tab. Anything else (`lazy`, `no-fade`,\n * `pills`, `<b-tab disabled>`'s tooltip behaviour) is not reimplemented.\n *\n * ## How registration works\n *\n * Children register themselves on mount and hand back a *render function* for\n * their title rather than a plain string. That is what lets a child's `#title`\n * slot be rendered inside the parent's nav strip, and it keeps a computed title\n * reactive: the parent invokes it inside its own render effect, so a title\n * change re-renders the nav.\n *\n * Mount order is document order for both static children and `v-for`-generated\n * ones, so the nav strip matches the source. A tab that unmounts (a closable\n * tab) hands the selection to its neighbour.\n *\n * ## Accessibility\n *\n * Implements the WAI-ARIA tabs pattern: `role=\"tablist\"` / `role=\"tab\"` /\n * `role=\"tabpanel\"`, `aria-selected`, `aria-controls` ↔ `aria-labelledby`\n * wiring, a roving `tabindex` (only the selected tab is in the tab order), and\n * Left/Right/Home/End keyboard navigation within the strip. Pass `aria-label`\n * (or `ariaLabel`) so screen-reader users hear what the strip is for.\n *\n * ## Counts and the \"More\" menu\n *\n * A `CnTab` with `count` shows that number after its title. A `CnTab` with\n * `overflow` is listed under a \"More\" menu beside the strip instead of in it:\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\">\n * <CnTab title=\"Documents\" :count=\"5\">…</CnTab>\n * <CnTab title=\"Archiving\" overflow>…</CnTab>\n * </CnTabs>\n * ```\n *\n * Picking a tab from the menu selects it and shows it in the strip for as long\n * as it is selected. With no overflow tab the menu does not render, and the\n * strip is exactly what it was before these props existed.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActions } from '@nextcloud/vue'\nimport { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, onUpdated, provide, reactive, ref, watch } from 'vue'\nimport ChevronDown from 'vue-material-design-icons/ChevronDown.vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { CN_TABS_INJECTION_KEY } from './tabsKey.js'\n\nexport default defineComponent({\n\tname: 'CnTabs',\n\n\tcomponents: {\n\t\tChevronDown,\n\t\tChevronLeft,\n\t\tChevronRight,\n\t\tNcActionButton,\n\t\tNcActions,\n\t},\n\n\tprops: {\n\t\t/** Extra class applied to the panel container (bootstrap-vue's `content-class`). */\n\t\tcontentClass: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Stretch the nav items to fill the strip (bootstrap-vue's `justified`). */\n\t\tjustified: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Card-style chrome (border + padding) around the panel area. */\n\t\tcard: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * How the strip is drawn: `line` (the default: folder tabs joined to\n\t\t * the panel below), `segmented` (a compact pill switch, for a view\n\t\t * switch such as \"My work / My team\") or `underline` (plain labels on\n\t\t * a rule, the open one underlined; board LpBeheerAlgemeen). An\n\t\t * `underline` strip wraps instead of scrolling. Roles and keyboard\n\t\t * behaviour are the same in all three.\n\t\t *\n\t\t * @type {'line'|'segmented'|'underline'}\n\t\t * @spec openspec/changes/admin-settings-underline-tabs/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-draw-an-underline-tab-strip\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'line',\n\t\t\tvalidator: (v) => ['line', 'segmented', 'underline'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip, applied to the `role=\"tablist\"`\n\t\t * element. Screen readers announce it when focus enters the strip.\n\t\t */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The look the strip is drawn in (`nextcloud` or `board`). Empty takes\n\t\t * the look the app provides. In the board look a `line` strip draws the\n\t\t * screens' folder tabs and never moves a tab under \"More\": the strip\n\t\t * scrolls instead.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t\t */\n\t\tlook: { // eslint-disable-line vue/no-unused-properties -- read through useLook(props) in setup\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Name of the menu that lists the `overflow` tabs. */\n\t\tmoreLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'More'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/** The selected tab changed. Payload: the new active tab's index. */\n\t\t'update:activeIndex',\n\t],\n\n\tsetup(props, { emit }) {\n\t\tconst { isBoard, lookClass: boardClass } = useLook(props)\n\t\t// The board strip: a `line` strip in the board look.\n\t\tconst boardStrip = computed(() => isBoard.value && props.variant === 'line')\n\t\t// Registered children, in mount order.\n\t\tconst tabs = reactive([])\n\t\tconst activeUid = ref(null)\n\t\tconst navButtons = ref([])\n\t\tconst nav = ref(null)\n\t\t// Whether the selection was chosen (a tab declared `active`, or the\n\t\t// user picked one) rather than defaulted to the first tab registered.\n\t\tlet selectionChosen = false\n\n\t\t/**\n\t\t * Register a child tab. The first child to register wins the initial\n\t\t * selection unless a later one declares itself `active`. Disabled tabs\n\t\t * never take the initial selection.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction register(tab) {\n\t\t\ttabs.push(tab)\n\t\t\tif (tab.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (tab.active) {\n\t\t\t\tactiveUid.value = tab.uid\n\t\t\t\tselectionChosen = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (activeUid.value === null) {\n\t\t\t\tactiveUid.value = tab.uid\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A tab under \"More\" only held the default selection because it\n\t\t\t// registered first. The first tab that is actually in the strip\n\t\t\t// takes it over, unless somebody chose.\n\t\t\tif (!selectionChosen && !tab.overflow) {\n\t\t\t\tconst current = tabs.find((candidate) => candidate.uid === activeUid.value)\n\t\t\t\tif (current && current.overflow) {\n\t\t\t\t\tactiveUid.value = tab.uid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Drop a child that is unmounting, moving the selection to its nearest\n\t\t * remaining neighbour when it was the active one. Closable tabs rely on\n\t\t * this; without it the strip would render with nothing selected.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction unregister(uid) {\n\t\t\tconst index = tabs.findIndex((tab) => tab.uid === uid)\n\t\t\tif (index !== -1) {\n\t\t\t\ttabs.splice(index, 1)\n\t\t\t}\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\tconst neighbour = tabs[index] || tabs[index - 1] || tabs[0] || null\n\t\t\t\tactiveUid.value = neighbour ? neighbour.uid : null\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Make a child the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction select(uid) {\n\t\t\tselectionChosen = true\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactiveUid.value = uid\n\t\t\temit('update:activeIndex', tabs.findIndex((tab) => tab.uid === uid))\n\t\t}\n\n\t\t/**\n\t\t * Whether a child is the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {boolean} True when this tab's panel should render.\n\t\t */\n\t\tfunction isActive(uid) {\n\t\t\treturn activeUid.value === uid\n\t\t}\n\n\t\tconst activeIndex = computed(() => tabs.findIndex((tab) => tab.uid === activeUid.value))\n\n\t\t/**\n\t\t * The tabs drawn in the strip: every tab that is not `overflow`, plus\n\t\t * an overflow tab for as long as it is the selected one.\n\t\t *\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tconst visibleTabs = computed(() => tabs.filter((tab) => boardStrip.value || !tab.overflow || tab.uid === activeUid.value))\n\n\t\t/**\n\t\t * The tabs listed under \"More\": overflow tabs that are not selected.\n\t\t *\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tconst overflowTabs = computed(() => (boardStrip.value ? [] : tabs.filter((tab) => tab.overflow && tab.uid !== activeUid.value)))\n\n\t\t/**\n\t\t * Whether a tab declares a count. `0` is a count; null and '' are not.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {boolean} True when the count renders.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tfunction hasCount(tab) {\n\t\t\treturn tab.count !== null && tab.count !== undefined && tab.count !== ''\n\t\t}\n\n\t\t/**\n\t\t * A \"More\" entry's text: the tab's title, and its count when it has one.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {string} The menu label.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tfunction overflowLabel(tab) {\n\t\t\treturn hasCount(tab) ? `${tab.label} (${tab.count})` : tab.label\n\t\t}\n\n\t\t/**\n\t\t * Select a tab picked from \"More\" and move focus to it in the strip,\n\t\t * so a keyboard user lands on the tab they chose rather than on a menu\n\t\t * button that no longer lists it.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tfunction onOverflowPick(tab) {\n\t\t\ttab.onActivate()\n\t\t\tnextTick(() => {\n\t\t\t\tconst button = nav.value?.querySelector(`#${tab.tabId}`)\n\t\t\t\tif (button && typeof button.focus === 'function') {\n\t\t\t\t\tbutton.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tconst moreAriaLabel = computed(() => t('nextcloud-vue', 'More tabs'))\n\n\t\t/**\n\t\t * Move the selection and DOM focus to a tab by index, skipping disabled\n\t\t * ones. Focus has to move with the selection: in the WAI-ARIA tabs\n\t\t * pattern only the selected tab is in the tab order, so leaving focus on\n\t\t * a now-`tabindex=\"-1\"` button strands the keyboard user.\n\t\t *\n\t\t * @param {number} index Target index (clamped to the enabled tabs).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction focusTab(index) {\n\t\t\t// The strip's own tabs only: the arrow keys walk what is drawn, and\n\t\t\t// the \"More\" menu has its own keyboard handling.\n\t\t\tconst enabled = visibleTabs.value.filter((tab) => !tab.disabled)\n\t\t\tif (enabled.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst wrapped = ((index % enabled.length) + enabled.length) % enabled.length\n\t\t\tconst target = enabled[wrapped]\n\t\t\tconst position = visibleTabs.value.indexOf(target)\n\t\t\tselect(target.uid)\n\t\t\tconst button = navButtons.value?.[position]\n\t\t\tif (button && typeof button.focus === 'function') {\n\t\t\t\tbutton.focus()\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Left/Right/Home/End navigation inside the strip.\n\t\t *\n\t\t * @param {KeyboardEvent} event The keydown event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavKeydown(event) {\n\t\t\tconst enabled = visibleTabs.value.filter((tab) => !tab.disabled)\n\t\t\tconst current = enabled.findIndex((tab) => tab.uid === activeUid.value)\n\t\t\tlet next = null\n\n\t\t\tif (event.key === 'ArrowRight') {\n\t\t\t\tnext = current + 1\n\t\t\t} else if (event.key === 'ArrowLeft') {\n\t\t\t\tnext = current - 1\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = enabled.length - 1\n\t\t\t}\n\n\t\t\tif (next === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tfocusTab(next)\n\t\t}\n\n\t\t/**\n\t\t * Scroll the strip so the selected tab is wholly in view.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction revealActiveTab() {\n\t\t\tconst strip = nav.value\n\t\t\tconst button = strip?.querySelector('[role=\"tab\"][aria-selected=\"true\"]')\n\t\t\tif (!strip || !button) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stripRect = strip.getBoundingClientRect()\n\t\t\tconst rect = button.getBoundingClientRect()\n\t\t\tif (rect.left < stripRect.left) {\n\t\t\t\tstrip.scrollLeft -= stripRect.left - rect.left\n\t\t\t} else if (rect.right > stripRect.right) {\n\t\t\t\tstrip.scrollLeft += rect.right - stripRect.right\n\t\t\t}\n\t\t}\n\n\t\twatch(activeUid, revealActiveTab, { flush: 'post' })\n\n\t\t// Whether tabs are hidden past the start or end edge of the strip.\n\t\tconst moreStart = ref(false)\n\t\tconst moreEnd = ref(false)\n\n\t\t/**\n\t\t * Recompute which edges hide tabs; 1px of tolerance absorbs sub-pixel rounding.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction measureOverflow() {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmoreStart.value = strip.scrollLeft > 1\n\t\t\tmoreEnd.value = strip.scrollWidth - strip.clientWidth - strip.scrollLeft > 1\n\t\t}\n\n\t\tlet resizeObserver = null\n\n\t\tonMounted(() => {\n\t\t\tmeasureOverflow()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !nav.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresizeObserver = new ResizeObserver(measureOverflow)\n\t\t\tresizeObserver.observe(nav.value)\n\t\t})\n\n\t\t// A title change or a tab added can tip the strip into overflowing without resizing it.\n\t\tonUpdated(measureOverflow)\n\n\t\tonBeforeUnmount(() => {\n\t\t\tresizeObserver?.disconnect()\n\t\t\tresizeObserver = null\n\t\t})\n\n\t\t/**\n\t\t * Scroll the strip most of a viewport towards one edge.\n\t\t *\n\t\t * @param {number} direction -1 towards the start, 1 towards the end.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction scrollStrip(direction) {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstrip.scrollLeft += direction * Math.max(1, Math.round(strip.clientWidth * 0.6))\n\t\t}\n\n\t\t/**\n\t\t * Turn a vertical wheel into a horizontal scroll while the strip overflows.\n\t\t *\n\t\t * @param {WheelEvent} event The wheel event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavWheel(event) {\n\t\t\tconst strip = event.currentTarget\n\t\t\tif (strip.scrollWidth <= strip.clientWidth || event.deltaX !== 0 || event.deltaY === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tstrip.scrollLeft += event.deltaY\n\t\t}\n\n\t\tprovide(CN_TABS_INJECTION_KEY, { register, unregister, select, isActive })\n\n\t\treturn {\n\t\t\tboardStrip,\n\t\t\tboardClass,\n\t\t\ttabs,\n\t\t\tvisibleTabs,\n\t\t\toverflowTabs,\n\t\t\thasCount,\n\t\t\toverflowLabel,\n\t\t\tonOverflowPick,\n\t\t\tmoreAriaLabel,\n\t\t\tisActive,\n\t\t\tactiveIndex,\n\t\t\tnav,\n\t\t\tnavButtons,\n\t\t\tmoreStart,\n\t\t\tmoreEnd,\n\t\t\tmeasureOverflow,\n\t\t\tscrollStrip,\n\t\t\tonNavKeydown,\n\t\t\tonNavWheel,\n\t\t}\n\t},\n})\n</script>\n\n<style scoped>\n/* The bar owns the rule under the strip so a `#nav-end` surface sits ON the\n line rather than above a line that stops where the last tab does. With no\n nav-end content the nav is the bar's only child and the result is pixel\n identical to the rule living on the nav itself. */\n.cn-tabs__bar {\n\t/* Top-aligned so the `#nav-end` control sits on the tab row, not below it,\n\t should the two sides ever differ in height. */\n\talign-items: flex-start;\n\tborder-bottom: 1px solid var(--color-border);\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n/* The strip holds the scrolling nav and the edge affordances drawn over it. */\n.cn-tabs__strip {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tposition: relative;\n}\n\n.cn-tabs__nav {\n\tdisplay: flex;\n\tgap: 4px;\n\t/* One row that scrolls, never a wrap: in a narrow sidebar a nine-tab strip\n\t wrapped into a stack taller than the panel under it. The scrollbar is\n\t hidden because it would sit between the tabs and the bar's rule and break\n\t the open tab's join with its panel; a fade and a chevron on the clipped\n\t edge say there is more, and the strip follows the wheel. */\n\tflex-wrap: nowrap;\n\toverflow-x: auto;\n\toverflow-y: hidden;\n\tscroll-behavior: smooth;\n\tscrollbar-width: none;\n\t/* Overhang the bar's 1px rule so the open tab's own bottom edge, drawn in\n\t the panel's colour, covers it and the tab joins its panel. */\n\tmargin-bottom: -1px;\n}\n\n.cn-tabs__nav::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cn-tabs__nav {\n\t\tscroll-behavior: auto;\n\t}\n}\n\n/* A fade over the clipped edge, only on the side that hides tabs. Stops a\n pixel above the bar's rule so the rule keeps its full colour.\n\n z-index 2 so the fades outrank a lifted tab: CnTabsWidget puts the open tab\n at 1, and a tie goes to DOM order, which would draw that tab over the start\n fade (it precedes the tabs) while the end fade still covered it. */\n.cn-tabs__strip::before,\n.cn-tabs__strip::after {\n\tcontent: '';\n\tdisplay: none;\n\tinset-block: 0 1px;\n\tpointer-events: none;\n\tposition: absolute;\n\twidth: 56px;\n\tz-index: 2;\n}\n\n.cn-tabs__strip::before {\n\tbackground: linear-gradient(to right, var(--color-main-background) 30%, transparent);\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__strip::after {\n\tbackground: linear-gradient(to left, var(--color-main-background) 30%, transparent);\n\tinset-inline-end: 0;\n}\n\n.cn-tabs__strip--more-start::before,\n.cn-tabs__strip--more-end::after {\n\tdisplay: block;\n}\n\n/* Doubled selector for the same reason as `.cn-tabs__nav .cn-tabs__nav-item`:\n Nextcloud's own `button` rule would otherwise give this a border, a fill\n and a margin. */\n.cn-tabs__strip .cn-tabs__scroll {\n\talign-items: center;\n\tbackground: none;\n\tborder: 0;\n\tborder-radius: 0;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tdisplay: flex;\n\t/* Pushed down a little so the chevron sits level with the tab labels. */\n\tinset-block: 2px 0;\n\tjustify-content: center;\n\tmargin: 0;\n\tmin-height: 0;\n\tpadding: 0;\n\tposition: absolute;\n\twidth: 28px;\n\tz-index: 3;\n}\n\n/* Nextcloud's `button:hover` rule would otherwise fill the chevron. */\n.cn-tabs__strip .cn-tabs__scroll:hover,\n.cn-tabs__strip .cn-tabs__scroll:focus {\n\tbackground: none;\n}\n\n.cn-tabs__scroll--start {\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__scroll--end {\n\tinset-inline-end: 0;\n}\n\n/* The count behind a tab's title. Full-contrast text on the page background,\n so it reads on the idle tab's darker fill and on the open tab alike. */\n.cn-tabs__count {\n\tbackground-color: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-pill, 100px);\n\tcolor: var(--color-main-text);\n\tdisplay: inline-block;\n\tfont-size: 0.85em;\n\tline-height: 1.4;\n\tmargin-inline-start: 6px;\n\tmin-width: 1.4em;\n\tpadding: 0 6px;\n\ttext-align: center;\n}\n\n.cn-tabs__more {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: 0 0 auto;\n}\n\n/* Keeps a \"More\" entry's text on the menu's text column without drawing an icon. */\n.cn-tabs__more-spacer {\n\tdisplay: inline-block;\n\twidth: 20px;\n}\n\n.cn-tabs__nav-end {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\tgap: 4px;\n\t/* No min-height. This used to hard-code 38px to mirror the old nav item's\n\t vertical box, but the folder-tab restyle changed that box to 33px, and\n\t because the bar is `align-items: flex-start` the taller nav-end then drove\n\t the BAR's height. The bar's bottom rule sank 6px below the tab row, so the\n\t open tab no longer met the panel it is drawn as joined to. Letting the\n\t control size to its own content keeps the rule on the tab row whatever the\n\t nav item's box becomes, so the two cannot drift apart again. */\n\tmin-height: 0;\n}\n\n/* Folder tabs, not underlined labels. An underline is the weakest possible\n affordance on a strip that already carries icons: with nine tabs on a dossiq\n case the row read as a sentence of links, and only the 2px rule said which\n one was open. Giving the inactive tabs their own darker surface makes the\n strip legible as a control at a glance, and the active tab, sharing the\n panel's background and punching a hole in the bar's rule, reads as the sheet\n in front. */\n.cn-tabs__nav-item {\n\tbackground-color: var(--color-background-dark);\n\t/* `--color-border-dark`, not `--color-border`: the latter is the same\n\t #ededed as `--color-background-dark` in the stock light theme, so a tab\n\t drawn with it has no visible edge against its own fill. */\n\tborder: 1px solid var(--color-border-dark);\n\tborder-bottom: none;\n\tborder-radius: var(--border-radius-large, 8px) var(--border-radius-large, 8px) 0 0;\n\t/* Full-contrast label, not `--color-text-maxcontrast`. On the old\n\t transparent strip maxcontrast sat on white at 5.3:1; on this darker\n\t surface the same pair measures 4.55:1, which clears WCAG AA by 0.05 and\n\t would not survive a themed instance shifting either token. A real tab\n\t strip carries its selection in the surface and the weight anyway, so\n\t nothing is lost by giving every label the readable colour. */\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tfont-weight: normal;\n\tpadding: 8px 12px;\n\twhite-space: nowrap;\n\t/* Match the `#nav-end` control's own height. The bar is a flex row, so\n\t whichever side is taller sets the bar's height and therefore where its\n\t bottom rule lands. When the control was the taller one the rule sat below\n\t the tab row and showed as a line under the open tab, breaking the join to\n\t the panel. Sizing both sides the same is what keeps the rule ON the tab\n\t row; `inline-flex` centres the label inside the taller box. */\n\talign-items: center;\n\tbox-sizing: border-box;\n\tdisplay: inline-flex;\n\tmin-height: var(--default-clickable-area, 34px);\n}\n\n/* Nextcloud's server stylesheet gives every plain `button` a 3px bottom margin\n with a selector that outscores the scoped rule above, which floated the tabs\n above the bar's rule inside a real page. The parent class wins on\n specificity rather than on `!important` or source order. */\n.cn-tabs__nav .cn-tabs__nav-item {\n\tmargin-block-end: 0;\n}\n\n.cn-tabs__nav--justified .cn-tabs__nav-item {\n\tflex: 1 1 0;\n}\n\n.cn-tabs__nav-item:hover,\n.cn-tabs__nav-item:focus-visible {\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n/* Inside the box: the scrolling strip clips anything drawn outside a tab. */\n.cn-tabs__nav-item:focus-visible {\n\toutline-offset: -2px;\n}\n\n.cn-tabs__nav-item[disabled] {\n\tcursor: default;\n\topacity: 0.5;\n}\n\n.cn-tabs__nav-item[disabled]:hover {\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n}\n\n/* The open tab: the panel's own background, and a bottom border painted in\n that same background so it erases the bar's rule underneath and the tab\n joins the sheet below it. The primary-coloured top edge is what carries the\n selection at a glance, and it survives forced-colours mode, which drops\n background colours but keeps borders. */\n.cn-tabs__nav-item--active {\n\tbackground-color: var(--color-main-background);\n\tborder-bottom: 1px solid var(--color-main-background);\n\tborder-top: 2px solid var(--color-primary-element);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\t/* Keep the text baseline identical to an inactive tab despite the 1px\n\t thicker top border, so the strip does not jog when the selection moves. */\n\tpadding-top: 7px;\n}\n\n.cn-tabs__content {\n\tpadding-top: 12px;\n}\n\n.cn-tabs--card .cn-tabs__content {\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\tpadding: 12px;\n}\n\n/* Segmented variant: the strip is a pill switch that stands on its own, so\n the bar's rule and the tab-joins-panel treatment are both dropped. */\n.cn-tabs--segmented .cn-tabs__bar {\n\tborder-bottom: none;\n}\n\n.cn-tabs--segmented .cn-tabs__strip {\n\tflex: 0 1 auto;\n}\n\n.cn-tabs--segmented .cn-tabs__nav {\n\tgap: 4px;\n\tmargin-bottom: 0;\n\tpadding: 4px;\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 10px));\n\tbackground-color: var(--color-background-dark);\n}\n\n.cn-tabs--segmented .cn-tabs__nav-item {\n\tborder: 1px solid transparent;\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tbackground-color: transparent;\n\tfont-weight: 600;\n\tpadding: 6px 16px;\n}\n\n.cn-tabs--segmented .cn-tabs__nav-item:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n/* The open segment is the raised one. Its border survives forced-colours\n mode, which drops the background and the shadow. */\n.cn-tabs--segmented .cn-tabs__nav-item--active {\n\tborder: 1px solid var(--color-border-dark);\n\tbackground-color: var(--color-main-background);\n\tbox-shadow: 0 1px 2px var(--color-box-shadow);\n\tfont-weight: 600;\n\tpadding-top: 6px;\n}\n\n/* Underline variant (board LpBeheerAlgemeen): plain labels on the bar's rule,\n the open one carried by a 3px primary underline that sits on that rule. The\n strip wraps onto a second row rather than scrolling. */\n.cn-tabs--underline .cn-tabs__nav {\n\tflex-wrap: wrap;\n\toverflow: visible;\n}\n\n.cn-tabs--underline .cn-tabs__nav-item {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-bottom: 3px solid transparent;\n\tborder-radius: 0;\n\tcolor: var(--cn-board-text-soft, var(--color-main-text));\n\tfont-size: 14px;\n\tfont-weight: 500;\n\tgap: 8px;\n\tmin-height: 44px;\n\tpadding: 0 9px;\n}\n\n.cn-tabs--underline .cn-tabs__nav-item:hover,\n.cn-tabs--underline .cn-tabs__nav-item:focus-visible {\n\tbackground-color: transparent;\n\tborder-bottom-color: var(--color-border-dark);\n\tcolor: var(--color-main-text);\n}\n\n.cn-tabs--underline .cn-tabs__nav-item[disabled]:hover {\n\tbackground-color: transparent;\n\tborder-bottom-color: transparent;\n}\n\n.cn-tabs--underline .cn-tabs__nav-item--active,\n.cn-tabs--underline .cn-tabs__nav-item--active:hover,\n.cn-tabs--underline .cn-tabs__nav-item--active:focus-visible {\n\tborder-bottom: 3px solid var(--color-primary-element);\n\tcolor: var(--color-main-text);\n\tfont-weight: 700;\n\tpadding-top: 0;\n}\n\n.cn-tabs--underline .cn-tabs__count {\n\tmargin-inline-start: 0;\n}\n\n.cn-tabs--underline .cn-tabs__content {\n\tpadding-top: 20px;\n}\n</style>\n"],"names":["t"],"mappings":";;;;;;;;;AAoMA,aAAe,eAAe,CAAC;CAC9B,IAAI,EAAE,QAAQ;;CAEd,UAAU,EAAE;EACX,WAAW;EACX,WAAW;EACX,YAAY;AACZ,EAAA,cAAc;AACd,EAAA,SAAS;EACT;;AAED,CAAA,KAAK,EAAE;AACN;EACA,YAAY,EAAE;GACb,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;;AAED;EACA,SAAS,EAAE;GACV,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;;;EAGD,IAAI,EAAE;GACL,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;;AAED;;;;;;AAMC;AACA;AACA;AACA;;AAED,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,MAAM;GACf,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;GAChE;;AAED;;;;EAIA,SAAS,EAAE;GACV,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;;AAED;;;;AAIC;AACA;AACA;;AAED,EAAA,IAAI,EAAE;GACL,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;;AAED;EACA,SAAS,EAAE;GACV,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,MAAM,CAAC;GACzC;EACD;;AAED,CAAA,KAAK,EAAE;;AAEN,EAAA,oBAAoB;EACpB;;AAED,CAAA,KAAK,CAAC,KAAK,EAAE,EAAE,IAAG,EAAG,EAAE;EACtB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,UAAW,EAAA,GAAI,OAAO,CAAC,KAAK,EAAA;AACxD;AACA,EAAA,MAAM,UAAW,GAAE,QAAQ,CAAC,MAAM,OAAO,CAAC,KAAM,IAAG,KAAK,CAAC,OAAQ,KAAI,MAAM,EAAA;AAC3E;AACA,EAAA,MAAM,IAAK,GAAE,QAAQ,CAAC,EAAE,EAAA;EACxB,MAAM,SAAQ,GAAI,GAAG,CAAC,IAAI,EAAA;EAC1B,MAAM,aAAa,GAAG,CAAC,EAAE,EAAA;AACzB,EAAA,MAAM,GAAE,GAAI,GAAG,CAAC,IAAI,EAAA;;;EAGpB,IAAI,kBAAkB,MAAI;;AAE1B;;;;AAIC;AACA;AACA;;;AAGD,EAAA,SAAS,QAAQ,CAAC,GAAG,EAAE;GACtB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAA;AACb,GAAA,IAAI,GAAG,CAAC,QAAQ,EAAE;IACjB,MAAK;AACN,IAAA;AACA,GAAA,IAAI,GAAG,CAAC,MAAM,EAAE;IACf,SAAS,CAAC,QAAQ,GAAG,CAAC,IAAE;AACxB,IAAA,kBAAkB,KAAG;IACrB,MAAK;AACN,IAAA;GACA,IAAI,SAAS,CAAC,KAAI,KAAM,IAAI,EAAE;IAC7B,SAAS,CAAC,QAAQ,GAAG,CAAC,IAAE;IACxB,MAAK;AACN,IAAA;;;AAGA;AACA,GAAA,IAAI,CAAC,eAAc,IAAK,CAAC,GAAG,CAAC,QAAQ,EAAE;AACtC,IAAA,MAAM,OAAQ,GAAE,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,GAAI,KAAI,SAAS,CAAC,KAAK,EAAA;IAC1E,IAAI,OAAM,IAAK,OAAO,CAAC,QAAQ,EAAE;KAChC,SAAS,CAAC,QAAQ,GAAG,CAAC,IAAE;AACzB,KAAA;AACD,IAAA;AACD,GAAA;;AAEA;;;;AAIC;AACA;AACA;;;EAGD,SAAS,UAAU,CAAC,GAAG,EAAE;GACxB,MAAM,KAAM,GAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,GAAI,KAAI,GAAG,EAAA;AACrD,GAAA,IAAI,KAAM,KAAI,CAAC,CAAC,EAAE;AACjB,IAAA,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAA;AACrB,IAAA;GACA,IAAI,SAAS,CAAC,KAAM,KAAI,GAAG,EAAE;IAC5B,MAAM,YAAY,IAAI,CAAC,KAAK,CAAE,IAAG,IAAI,CAAC,KAAM,GAAE,CAAC,CAAA,IAAK,IAAI,CAAC,CAAC,CAAA,IAAK,KAAG;AAClE,IAAA,SAAS,CAAC,KAAI,GAAI,SAAU,GAAE,SAAS,CAAC,MAAM,KAAG;AAClD,IAAA;AACD,GAAA;;AAEA;;AAEC;AACA;AACA;;;AAGD,EAAA,SAAS,MAAM,CAAC,GAAG,EAAE;AACpB,GAAA,kBAAkB,KAAG;GACrB,IAAI,SAAS,CAAC,KAAM,KAAI,GAAG,EAAE;IAC5B,MAAK;AACN,IAAA;AACA,GAAA,SAAS,CAAC,KAAM,GAAE,IAAE;AACpB,GAAA,IAAI,CAAC,oBAAoB,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,GAAE,KAAM,GAAG,CAAC,EAAA;AACpE,GAAA;;AAEA;;AAEC;AACA;AACA;;;AAGD,EAAA,SAAS,QAAQ,CAAC,GAAG,EAAE;GACtB,OAAO,SAAS,CAAC,UAAU,GAAE;AAC9B,GAAA;;AAEA,EAAA,MAAM,WAAU,GAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,GAAE,KAAM,SAAS,CAAC,KAAK,CAAC,EAAA;;AAEvF;;AAEC;AACA;AACA;;EAED,MAAM,WAAY,GAAE,QAAQ,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,UAAU,CAAC,KAAI,IAAK,CAAC,GAAG,CAAC,QAAS,IAAG,GAAG,CAAC,QAAQ,SAAS,CAAC,KAAK,CAAC,EAAA;;AAEzH;;AAEC;AACA;;EAED,MAAM,YAAa,GAAE,QAAQ,CAAC,OAAO,UAAU,CAAC,KAAI,GAAI,EAAG,GAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,QAAS,IAAG,GAAG,CAAC,QAAQ,SAAS,CAAC,KAAK,CAAC,CAAC,EAAA;;AAE/H;;AAEC;AACA;AACA;AACA;AACA;;AAED,EAAA,SAAS,QAAQ,CAAC,GAAG,EAAE;GACtB,OAAO,GAAG,CAAC,KAAM,KAAI,IAAG,IAAK,GAAG,CAAC,KAAI,KAAM,SAAQ,IAAK,GAAG,CAAC,KAAM,KAAI,EAAC;AACxE,GAAA;;AAEA;;AAEC;AACA;AACA;;AAEA;;EAED,SAAS,aAAa,CAAC,GAAG,EAAE;GAC3B,OAAO,QAAQ,CAAC,GAAG,CAAE,GAAE,CAAC,EAAE,GAAG,CAAC,KAAK,CAAA,EAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA,GAAI,GAAG,CAAC,KAAI;AAChE,GAAA;;AAEA;;;;AAIC;AACA;AACA;;AAEA;;EAED,SAAS,cAAc,CAAC,GAAG,EAAE;AAC5B,GAAA,GAAG,CAAC,UAAU,GAAC;GACf,QAAQ,CAAC,MAAM;IACd,MAAM,MAAO,GAAE,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAA;AACvD,IAAA,IAAI,MAAO,IAAG,OAAO,MAAM,CAAC,KAAI,KAAM,UAAU,EAAE;KACjD,MAAM,CAAC,KAAK,GAAC;AACd,KAAA;IACA,EAAA;AACF,GAAA;;EAEA,MAAM,aAAY,GAAI,QAAQ,CAAC,MAAMA,SAAC,CAAC,eAAe,EAAE,WAAW,CAAC,EAAA;;AAEpE;;;;;AAKC;;AAEA;;;EAGD,SAAS,QAAQ,CAAC,KAAK,EAAE;;AAExB;GACA,MAAM,OAAM,GAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAA;GAC/D,IAAI,OAAO,CAAC,MAAK,KAAM,CAAC,EAAE;IACzB,MAAK;AACN,IAAA;AACA,GAAA,MAAM,OAAQ,GAAE,CAAC,CAAC,KAAI,GAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAK;GAC3E,MAAM,MAAK,GAAI,OAAO,CAAC,OAAO,EAAA;GAC9B,MAAM,QAAS,GAAE,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAA;AACjD,GAAA,MAAM,CAAC,MAAM,CAAC,GAAG,EAAA;AACjB,GAAA,MAAM,MAAK,GAAI,UAAU,CAAC,KAAK,GAAG,QAAQ,EAAA;AAC1C,GAAA,IAAI,MAAO,IAAG,OAAO,MAAM,CAAC,KAAI,KAAM,UAAU,EAAE;IACjD,MAAM,CAAC,KAAK,GAAC;AACd,IAAA;AACD,GAAA;;AAEA;AACC;AACA;AACA;AACA;;;EAGD,SAAS,YAAY,CAAC,KAAK,EAAE;GAC5B,MAAM,OAAM,GAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAA;AAC/D,GAAA,MAAM,OAAQ,GAAE,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,GAAE,KAAM,SAAS,CAAC,KAAK,EAAA;GACtE,IAAI,IAAK,GAAE,KAAG;;GAEd,IAAI,KAAK,CAAC,GAAE,KAAM,YAAY,EAAE;IAC/B,IAAG,GAAI,OAAM,GAAI,EAAA;AAClB,IAAA,MAAO,IAAI,KAAK,CAAC,GAAI,KAAI,WAAW,EAAE;IACrC,IAAG,GAAI,OAAM,GAAI,EAAA;IAChB,MAAK,IAAI,KAAK,CAAC,GAAI,KAAI,MAAM,EAAE;IAChC,IAAG,GAAI,EAAA;IACN,MAAK,IAAI,KAAK,CAAC,GAAI,KAAI,KAAK,EAAE;AAC/B,IAAA,IAAG,GAAI,OAAO,CAAC,SAAS,EAAA;AACzB,IAAA;;AAEA,GAAA,IAAI,IAAG,KAAM,IAAI,EAAE;IAClB,MAAK;AACN,IAAA;AACA,GAAA,KAAK,CAAC,cAAc,GAAC;GACrB,QAAQ,CAAC,IAAI,EAAA;AACd,GAAA;;AAEA;AACC;AACA;;;EAGD,SAAS,eAAe,GAAG;AAC1B,GAAA,MAAM,KAAM,GAAE,GAAG,CAAC,MAAI;AACtB,GAAA,MAAM,MAAK,GAAI,KAAK,EAAE,aAAa,CAAC,oCAAoC,EAAA;AACxE,GAAA,IAAI,CAAC,KAAI,IAAK,CAAC,MAAM,EAAE;IACtB,MAAK;AACN,IAAA;AACA,GAAA,MAAM,SAAU,GAAE,KAAK,CAAC,qBAAqB,GAAC;AAC9C,GAAA,MAAM,IAAG,GAAI,MAAM,CAAC,qBAAqB,GAAC;GAC1C,IAAI,IAAI,CAAC,IAAG,GAAI,SAAS,CAAC,IAAI,EAAE;AAC/B,IAAA,KAAK,CAAC,UAAW,IAAG,SAAS,CAAC,IAAK,GAAE,IAAI,CAAC,KAAG;AAC9C,IAAA,MAAO,IAAI,IAAI,CAAC,KAAM,GAAE,SAAS,CAAC,KAAK,EAAE;AACxC,IAAA,KAAK,CAAC,cAAc,IAAI,CAAC,KAAM,GAAE,SAAS,CAAC,MAAI;AAChD,IAAA;AACD,GAAA;;EAEA,KAAK,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAA;;;EAGnD,MAAM,SAAU,GAAE,GAAG,CAAC,KAAK,EAAA;EAC3B,MAAM,UAAU,GAAG,CAAC,KAAK,EAAA;;AAEzB;AACC;AACA;;;EAGD,SAAS,eAAe,GAAG;AAC1B,GAAA,MAAM,KAAM,GAAE,GAAG,CAAC,MAAI;GACtB,IAAI,CAAC,KAAK,EAAE;IACX,MAAK;AACN,IAAA;AACA,GAAA,SAAS,CAAC,KAAM,GAAE,KAAK,CAAC,aAAa,EAAA;AACrC,GAAA,OAAO,CAAC,KAAI,GAAI,KAAK,CAAC,cAAc,KAAK,CAAC,WAAY,GAAE,KAAK,CAAC,aAAa,EAAA;AAC5E,GAAA;;AAEA,EAAA,IAAI,iBAAiB,KAAG;;AAExB,EAAA,SAAS,CAAC,MAAM;AACf,GAAA,eAAe,GAAC;GAChB,IAAI,OAAO,cAAa,KAAM,WAAY,IAAG,CAAC,GAAG,CAAC,KAAK,EAAE;IACxD,MAAK;AACN,IAAA;GACA,cAAa,GAAI,IAAI,cAAc,CAAC,eAAe,EAAA;AACnD,GAAA,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAA;GAChC,EAAA;;AAED;EACA,SAAS,CAAC,eAAe,EAAA;;AAEzB,EAAA,eAAe,CAAC,MAAM;GACrB,cAAc,EAAE,UAAU,GAAC;AAC3B,GAAA,cAAa,GAAI,KAAG;GACpB,EAAA;;AAED;AACC;AACA;;AAEA;;;AAGD,EAAA,SAAS,WAAW,CAAC,SAAS,EAAE;AAC/B,GAAA,MAAM,KAAM,GAAE,GAAG,CAAC,MAAI;GACtB,IAAI,CAAC,KAAK,EAAE;IACX,MAAK;AACN,IAAA;AACA,GAAA,KAAK,CAAC,UAAW,IAAG,SAAU,GAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAY,GAAE,GAAG,CAAC,EAAA;AAChF,GAAA;;AAEA;AACC;AACA;AACA;AACA;;;EAGD,SAAS,UAAU,CAAC,KAAK,EAAE;GAC1B,MAAM,KAAI,GAAI,KAAK,CAAC,cAAY;AAChC,GAAA,IAAI,KAAK,CAAC,WAAU,IAAK,KAAK,CAAC,WAAY,IAAG,KAAK,CAAC,MAAO,KAAI,CAAA,IAAK,KAAK,CAAC,MAAO,KAAI,CAAC,EAAE;IACvF,MAAK;AACN,IAAA;AACA,GAAA,KAAK,CAAC,cAAc,GAAC;GACrB,KAAK,CAAC,UAAS,IAAK,KAAK,CAAC,OAAK;AAChC,GAAA;;AAEA,EAAA,OAAO,CAAC,qBAAqB,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAA;;AAEzE,EAAA,OAAO;GACN,UAAU;GACV,UAAU;GACV,IAAI;GACJ,WAAW;GACX,YAAY;AACZ,GAAA,QAAQ;GACR,aAAa;AACb,GAAA,cAAc;GACd,aAAa;AACb,GAAA,QAAQ;GACR,WAAW;AACX,GAAA,GAAG;GACH,UAAU;AACV,GAAA,SAAS;AACT,GAAA,OAAO;AACP,GAAA,eAAe;GACf,WAAW;GACX,YAAY;GACZ,UAAU;AACX,GAAA;EACA;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"CnTabs.vue2.js","sources":["../../../../src/components/CnTabs/CnTabs.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-tabs\" :class=\"[{ 'cn-tabs--card': card, 'cn-tabs--segmented': variant === 'segmented', 'cn-tabs--underline': variant === 'underline', 'cn-tabs--board': boardStrip }, boardClass]\">\n\t\t<div class=\"cn-tabs__bar\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-tabs__strip\"\n\t\t\t\t:class=\"{ 'cn-tabs__strip--more-start': moreStart, 'cn-tabs__strip--more-end': moreEnd }\">\n\t\t\t\t<div\n\t\t\t\t\tref=\"nav\"\n\t\t\t\t\tclass=\"cn-tabs__nav\"\n\t\t\t\t\t:class=\"{ 'cn-tabs__nav--justified': justified }\"\n\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t:aria-label=\"ariaLabel || null\"\n\t\t\t\t\t@keydown=\"onNavKeydown\"\n\t\t\t\t\t@wheel=\"onNavWheel\"\n\t\t\t\t\t@scroll=\"measureOverflow\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-for=\"tab in visibleTabs\"\n\t\t\t\t\t\t:id=\"tab.tabId\"\n\t\t\t\t\t\t:key=\"tab.uid\"\n\t\t\t\t\t\tref=\"navButtons\"\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\tclass=\"cn-tabs__nav-item\"\n\t\t\t\t\t\t:class=\"{ 'cn-tabs__nav-item--active': isActive(tab.uid) }\"\n\t\t\t\t\t\t:aria-selected=\"isActive(tab.uid) ? 'true' : 'false'\"\n\t\t\t\t\t\t:aria-controls=\"tab.panelId\"\n\t\t\t\t\t\t:tabindex=\"isActive(tab.uid) ? 0 : -1\"\n\t\t\t\t\t\t:disabled=\"tab.disabled || null\"\n\t\t\t\t\t\t@click=\"tab.onActivate()\">\n\t\t\t\t\t\t<!-- The space before the count is load-bearing: it keeps\n\t\t\t\t\t\t the tab's accessible name \"Documents 5\" rather than\n\t\t\t\t\t\t \"Documents5\". The margin separates the two for the eye\n\t\t\t\t\t\t only, so keep both tags on one line. -->\n\t\t\t\t\t\t<component :is=\"tab.titleRender\" /> <span\n\t\t\t\t\t\t\tv-if=\"hasCount(tab)\"\n\t\t\t\t\t\t\tclass=\"cn-tabs__count\"\n\t\t\t\t\t\t\tdata-testid=\"cn-tabs-count\">{{ tab.count }}</span>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t\t<!-- Mouse-only affordances: keyboard users already have the arrow\n\t\t\t\t keys, and the tabs themselves stay in the accessibility tree. -->\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreStart\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--start\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(-1)\">\n\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"moreEnd\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-tabs__scroll cn-tabs__scroll--end\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t@click=\"scrollStrip(1)\">\n\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<!-- The tabs that did not make the strip. OUTSIDE the tablist, like\n\t\t\t `#nav-end`: a menu button nested in it would be announced as\n\t\t\t one more tab. A tab picked here joins the strip while it is\n\t\t\t selected, so the open panel always has a real tab to be\n\t\t\t labelled by. -->\n\t\t\t<div v-if=\"overflowTabs.length > 0\" class=\"cn-tabs__more\">\n\t\t\t\t<NcActions\n\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t:forceName=\"true\"\n\t\t\t\t\t:menuName=\"moreLabel\"\n\t\t\t\t\t:aria-label=\"moreAriaLabel\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\tdata-testid=\"cn-tabs-more\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ChevronDown :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\tv-for=\"tab in overflowTabs\"\n\t\t\t\t\t\t:key=\"tab.uid\"\n\t\t\t\t\t\t:disabled=\"tab.disabled || null\"\n\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-tabs-more-item\"\n\t\t\t\t\t\t@click=\"onOverflowPick(tab)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<span class=\"cn-tabs__more-spacer\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ overflowLabel(tab) }}\n\t\t\t\t\t</NcActionButton>\n\t\t\t\t</NcActions>\n\t\t\t</div>\n\t\t\t<div v-if=\"$slots['nav-end']\" class=\"cn-tabs__nav-end\">\n\t\t\t\t<!-- @slot nav-end Rendered at the right-hand end of the tab bar, deliberately OUTSIDE the `role=\"tablist\"` element. A widget Actions menu belongs beside the strip, not inside it: anything nested in the tablist is announced as one of the tabs, so a screen-reader user counting six tabs would hear seven. -->\n\t\t\t\t<!-- @binding {number} active-index Index of the currently selected tab. -->\n\t\t\t\t<slot name=\"nav-end\" :activeIndex=\"activeIndex\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"cn-tabs__content\" :class=\"contentClass\">\n\t\t\t<!-- @slot The CnTab children. Anything else is rendered into the panel area untouched. -->\n\t\t\t<slot />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * CnTabs — a generic tab strip.\n *\n * Neither `@nextcloud/vue@9` nor this library previously shipped one: the only\n * tab component in either is `NcAppSidebarTab`, which is meaningless outside an\n * `NcAppSidebar`. Apps that needed a plain tab strip in a page, a dialog or a\n * detail view were on `bootstrap-vue@2` — a Vue-2-only package with no Vue 3\n * release (`bootstrap-vue-next` is a different package with a different API) —\n * and pulling a second UI framework in alongside `@nextcloud/vue` cuts against\n * the fleet rule that apps render with Nextcloud components. So zaakafhandelapp\n * wrote a local `Tabs`/`Tab` pair for eight views and opencatalogi and\n * openregister flagged the same gap. This is that pair, lifted and hardened.\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\" justified>\n * <CnTab title=\"Documents\">…</CnTab>\n * <CnTab title=\"Tasks\" :active=\"showTasks\" @click=\"onTasks\">…</CnTab>\n * <CnTab>\n * <template #title><CnIcon name=\"close\" /> Draft</template>\n * …\n * </CnTab>\n * </CnTabs>\n * ```\n *\n * ## Putting controls beside the strip\n *\n * `#nav-end` fills the right-hand end of the tab bar. `CnTabsWidget` uses it to\n * carry the active child's Actions menu, so one menu serves every tab instead\n * of each panel drawing its own header:\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\">\n * <template #nav-end><CnActionsMenu :title=\"activeTitle\" /></template>\n * <CnTab title=\"Notes\">…</CnTab>\n * </CnTabs>\n * ```\n *\n * The slot renders OUTSIDE the `role=\"tablist\"` element on purpose. See the\n * comment on it in the template.\n *\n * ## Migrating from `bootstrap-vue`\n *\n * `<BTabs>`/`<BTab>` map across directly for the subset apps actually used:\n * `content-class`, `justified` and `card` on the strip; `title`, the `#title`\n * slot, `active` and `@click` on a tab. Anything else (`lazy`, `no-fade`,\n * `pills`, `<b-tab disabled>`'s tooltip behaviour) is not reimplemented.\n *\n * ## How registration works\n *\n * Children register themselves on mount and hand back a *render function* for\n * their title rather than a plain string. That is what lets a child's `#title`\n * slot be rendered inside the parent's nav strip, and it keeps a computed title\n * reactive: the parent invokes it inside its own render effect, so a title\n * change re-renders the nav.\n *\n * Mount order is document order for both static children and `v-for`-generated\n * ones, so the nav strip matches the source. A tab that unmounts (a closable\n * tab) hands the selection to its neighbour.\n *\n * ## Accessibility\n *\n * Implements the WAI-ARIA tabs pattern: `role=\"tablist\"` / `role=\"tab\"` /\n * `role=\"tabpanel\"`, `aria-selected`, `aria-controls` ↔ `aria-labelledby`\n * wiring, a roving `tabindex` (only the selected tab is in the tab order), and\n * Left/Right/Home/End keyboard navigation within the strip. Pass `aria-label`\n * (or `ariaLabel`) so screen-reader users hear what the strip is for.\n *\n * ## Counts and the \"More\" menu\n *\n * A `CnTab` with `count` shows that number after its title. A `CnTab` with\n * `overflow` is listed under a \"More\" menu beside the strip instead of in it:\n *\n * ```vue\n * <CnTabs aria-label=\"Case details\">\n * <CnTab title=\"Documents\" :count=\"5\">…</CnTab>\n * <CnTab title=\"Archiving\" overflow>…</CnTab>\n * </CnTabs>\n * ```\n *\n * Picking a tab from the menu selects it and shows it in the strip for as long\n * as it is selected. With no overflow tab the menu does not render, and the\n * strip is exactly what it was before these props existed.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActions } from '@nextcloud/vue'\nimport { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, onUpdated, provide, reactive, ref, watch } from 'vue'\nimport ChevronDown from 'vue-material-design-icons/ChevronDown.vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { CN_TABS_INJECTION_KEY } from './tabsKey.js'\n\nexport default defineComponent({\n\tname: 'CnTabs',\n\n\tcomponents: {\n\t\tChevronDown,\n\t\tChevronLeft,\n\t\tChevronRight,\n\t\tNcActionButton,\n\t\tNcActions,\n\t},\n\n\tprops: {\n\t\t/** Extra class applied to the panel container (bootstrap-vue's `content-class`). */\n\t\tcontentClass: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Stretch the nav items to fill the strip (bootstrap-vue's `justified`). */\n\t\tjustified: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Card-style chrome (border + padding) around the panel area. */\n\t\tcard: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * How the strip is drawn: `line` (the default: folder tabs joined to\n\t\t * the panel below), `segmented` (a compact pill switch, for a view\n\t\t * switch such as \"My work / My team\") or `underline` (plain labels on\n\t\t * a rule, the open one underlined; board LpBeheerAlgemeen). An\n\t\t * `underline` strip wraps instead of scrolling. Roles and keyboard\n\t\t * behaviour are the same in all three.\n\t\t *\n\t\t * @type {'line'|'segmented'|'underline'}\n\t\t * @spec openspec/changes/admin-settings-underline-tabs/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-draw-an-underline-tab-strip\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'line',\n\t\t\tvalidator: (v) => ['line', 'segmented', 'underline'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip, applied to the `role=\"tablist\"`\n\t\t * element. Screen readers announce it when focus enters the strip.\n\t\t */\n\t\tariaLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The look the strip is drawn in (`nextcloud` or `board`). Empty takes\n\t\t * the look the app provides. In the board look a `line` strip draws the\n\t\t * screens' folder tabs and never moves a tab under \"More\": the strip\n\t\t * scrolls instead.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t\t */\n\t\tlook: { // eslint-disable-line vue/no-unused-properties -- read through useLook(props) in setup\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Leave the `overflow` tabs to the host's `#nav-end` menu instead of a\n\t\t * \"More\" menu of the strip's own. The strip then draws only the tabs\n\t\t * that are not `overflow`, and an overflow tab that is selected joins\n\t\t * it just before the last of them (board DqZaakBesluiten: Overview,\n\t\t * Files, Contact, Tasks, Decisions, History). The host lists the\n\t\t * overflow tabs and selects one through its `CnTab`'s `active`.\n\t\t *\n\t\t * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-board-tabs-list-their-overflow-in-the-actions-menu\n\t\t */\n\t\toverflowInNavEnd: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Name of the menu that lists the `overflow` tabs. */\n\t\tmoreLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'More'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/** The selected tab changed. Payload: the new active tab's index. */\n\t\t'update:activeIndex',\n\t],\n\n\tsetup(props, { emit }) {\n\t\tconst { isBoard, lookClass: boardClass } = useLook(props)\n\t\t// The board strip: a `line` strip in the board look.\n\t\tconst boardStrip = computed(() => isBoard.value && props.variant === 'line')\n\t\t// Registered children, in mount order.\n\t\tconst tabs = reactive([])\n\t\tconst activeUid = ref(null)\n\t\tconst navButtons = ref([])\n\t\tconst nav = ref(null)\n\t\t// Whether the selection was chosen (a tab declared `active`, or the\n\t\t// user picked one) rather than defaulted to the first tab registered.\n\t\tlet selectionChosen = false\n\n\t\t/**\n\t\t * Register a child tab. The first child to register wins the initial\n\t\t * selection unless a later one declares itself `active`. Disabled tabs\n\t\t * never take the initial selection.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction register(tab) {\n\t\t\ttabs.push(tab)\n\t\t\tif (tab.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (tab.active) {\n\t\t\t\tactiveUid.value = tab.uid\n\t\t\t\tselectionChosen = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (activeUid.value === null) {\n\t\t\t\tactiveUid.value = tab.uid\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A tab under \"More\" only held the default selection because it\n\t\t\t// registered first. The first tab that is actually in the strip\n\t\t\t// takes it over, unless somebody chose.\n\t\t\tif (!selectionChosen && !tab.overflow) {\n\t\t\t\tconst current = tabs.find((candidate) => candidate.uid === activeUid.value)\n\t\t\t\tif (current && current.overflow) {\n\t\t\t\t\tactiveUid.value = tab.uid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Drop a child that is unmounting, moving the selection to its nearest\n\t\t * remaining neighbour when it was the active one. Closable tabs rely on\n\t\t * this; without it the strip would render with nothing selected.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction unregister(uid) {\n\t\t\tconst index = tabs.findIndex((tab) => tab.uid === uid)\n\t\t\tif (index !== -1) {\n\t\t\t\ttabs.splice(index, 1)\n\t\t\t}\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\tconst neighbour = tabs[index] || tabs[index - 1] || tabs[0] || null\n\t\t\t\tactiveUid.value = neighbour ? neighbour.uid : null\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Make a child the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction select(uid) {\n\t\t\tselectionChosen = true\n\t\t\tif (activeUid.value === uid) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactiveUid.value = uid\n\t\t\temit('update:activeIndex', tabs.findIndex((tab) => tab.uid === uid))\n\t\t}\n\n\t\t/**\n\t\t * Whether a child is the visible one.\n\t\t *\n\t\t * @param {number} uid The child's instance uid.\n\t\t *\n\t\t * @return {boolean} True when this tab's panel should render.\n\t\t */\n\t\tfunction isActive(uid) {\n\t\t\treturn activeUid.value === uid\n\t\t}\n\n\t\tconst activeIndex = computed(() => tabs.findIndex((tab) => tab.uid === activeUid.value))\n\n\t\t/**\n\t\t * The tabs drawn in the strip: every tab that is not `overflow`, plus\n\t\t * an overflow tab for as long as it is the selected one.\n\t\t *\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tconst visibleTabs = computed(() => {\n\t\t\tif (props.overflowInNavEnd) {\n\t\t\t\treturn navEndStrip()\n\t\t\t}\n\t\t\treturn tabs.filter((tab) => boardStrip.value || !tab.overflow || tab.uid === activeUid.value)\n\t\t})\n\n\t\t/**\n\t\t * The strip under `overflowInNavEnd`: the tabs that are not\n\t\t * `overflow`, with a selected overflow tab placed before the last.\n\t\t *\n\t\t * @return {object[]} The tabs to draw, in order.\n\t\t * @spec openspec/changes/board-tabs-overflow-in-actions/specs/board-tabs-overflow-in-actions/spec.md#requirement-an-opened-overflow-tab-joins-the-strip-before-the-last-tab\n\t\t */\n\t\tfunction navEndStrip() {\n\t\t\tconst pinned = tabs.filter((tab) => !tab.overflow)\n\t\t\tconst opened = tabs.find((tab) => tab.overflow && tab.uid === activeUid.value)\n\t\t\tif (!opened) {\n\t\t\t\treturn pinned\n\t\t\t}\n\t\t\tconst at = Math.max(0, pinned.length - 1)\n\t\t\treturn [...pinned.slice(0, at), opened, ...pinned.slice(at)]\n\t\t}\n\n\t\t/**\n\t\t * The tabs listed under \"More\": overflow tabs that are not selected.\n\t\t *\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tconst overflowTabs = computed(() => (boardStrip.value || props.overflowInNavEnd ? [] : tabs.filter((tab) => tab.overflow && tab.uid !== activeUid.value)))\n\n\t\t/**\n\t\t * Whether a tab declares a count. `0` is a count; null and '' are not.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {boolean} True when the count renders.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tfunction hasCount(tab) {\n\t\t\treturn tab.count !== null && tab.count !== undefined && tab.count !== ''\n\t\t}\n\n\t\t/**\n\t\t * A \"More\" entry's text: the tab's title, and its count when it has one.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {string} The menu label.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tfunction overflowLabel(tab) {\n\t\t\treturn hasCount(tab) ? `${tab.label} (${tab.count})` : tab.label\n\t\t}\n\n\t\t/**\n\t\t * Select a tab picked from \"More\" and move focus to it in the strip,\n\t\t * so a keyboard user lands on the tab they chose rather than on a menu\n\t\t * button that no longer lists it.\n\t\t *\n\t\t * @param {object} tab Child descriptor.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tfunction onOverflowPick(tab) {\n\t\t\ttab.onActivate()\n\t\t\tnextTick(() => {\n\t\t\t\tconst button = nav.value?.querySelector(`#${tab.tabId}`)\n\t\t\t\tif (button && typeof button.focus === 'function') {\n\t\t\t\t\tbutton.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tconst moreAriaLabel = computed(() => t('nextcloud-vue', 'More tabs'))\n\n\t\t/**\n\t\t * Move the selection and DOM focus to a tab by index, skipping disabled\n\t\t * ones. Focus has to move with the selection: in the WAI-ARIA tabs\n\t\t * pattern only the selected tab is in the tab order, so leaving focus on\n\t\t * a now-`tabindex=\"-1\"` button strands the keyboard user.\n\t\t *\n\t\t * @param {number} index Target index (clamped to the enabled tabs).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction focusTab(index) {\n\t\t\t// The strip's own tabs only: the arrow keys walk what is drawn, and\n\t\t\t// the \"More\" menu has its own keyboard handling.\n\t\t\tconst enabled = visibleTabs.value.filter((tab) => !tab.disabled)\n\t\t\tif (enabled.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst wrapped = ((index % enabled.length) + enabled.length) % enabled.length\n\t\t\tconst target = enabled[wrapped]\n\t\t\tconst position = visibleTabs.value.indexOf(target)\n\t\t\tselect(target.uid)\n\t\t\tconst button = navButtons.value?.[position]\n\t\t\tif (button && typeof button.focus === 'function') {\n\t\t\t\tbutton.focus()\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Left/Right/Home/End navigation inside the strip.\n\t\t *\n\t\t * @param {KeyboardEvent} event The keydown event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavKeydown(event) {\n\t\t\tconst enabled = visibleTabs.value.filter((tab) => !tab.disabled)\n\t\t\tconst current = enabled.findIndex((tab) => tab.uid === activeUid.value)\n\t\t\tlet next = null\n\n\t\t\tif (event.key === 'ArrowRight') {\n\t\t\t\tnext = current + 1\n\t\t\t} else if (event.key === 'ArrowLeft') {\n\t\t\t\tnext = current - 1\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = enabled.length - 1\n\t\t\t}\n\n\t\t\tif (next === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tfocusTab(next)\n\t\t}\n\n\t\t/**\n\t\t * Scroll the strip so the selected tab is wholly in view.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction revealActiveTab() {\n\t\t\tconst strip = nav.value\n\t\t\tconst button = strip?.querySelector('[role=\"tab\"][aria-selected=\"true\"]')\n\t\t\tif (!strip || !button) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stripRect = strip.getBoundingClientRect()\n\t\t\tconst rect = button.getBoundingClientRect()\n\t\t\tif (rect.left < stripRect.left) {\n\t\t\t\tstrip.scrollLeft -= stripRect.left - rect.left\n\t\t\t} else if (rect.right > stripRect.right) {\n\t\t\t\tstrip.scrollLeft += rect.right - stripRect.right\n\t\t\t}\n\t\t}\n\n\t\twatch(activeUid, revealActiveTab, { flush: 'post' })\n\n\t\t// Whether tabs are hidden past the start or end edge of the strip.\n\t\tconst moreStart = ref(false)\n\t\tconst moreEnd = ref(false)\n\n\t\t/**\n\t\t * Recompute which edges hide tabs; 1px of tolerance absorbs sub-pixel rounding.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction measureOverflow() {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmoreStart.value = strip.scrollLeft > 1\n\t\t\tmoreEnd.value = strip.scrollWidth - strip.clientWidth - strip.scrollLeft > 1\n\t\t}\n\n\t\tlet resizeObserver = null\n\n\t\tonMounted(() => {\n\t\t\tmeasureOverflow()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !nav.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresizeObserver = new ResizeObserver(measureOverflow)\n\t\t\tresizeObserver.observe(nav.value)\n\t\t})\n\n\t\t// A title change or a tab added can tip the strip into overflowing without resizing it.\n\t\tonUpdated(measureOverflow)\n\n\t\tonBeforeUnmount(() => {\n\t\t\tresizeObserver?.disconnect()\n\t\t\tresizeObserver = null\n\t\t})\n\n\t\t/**\n\t\t * Scroll the strip most of a viewport towards one edge.\n\t\t *\n\t\t * @param {number} direction -1 towards the start, 1 towards the end.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction scrollStrip(direction) {\n\t\t\tconst strip = nav.value\n\t\t\tif (!strip) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstrip.scrollLeft += direction * Math.max(1, Math.round(strip.clientWidth * 0.6))\n\t\t}\n\n\t\t/**\n\t\t * Turn a vertical wheel into a horizontal scroll while the strip overflows.\n\t\t *\n\t\t * @param {WheelEvent} event The wheel event.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tfunction onNavWheel(event) {\n\t\t\tconst strip = event.currentTarget\n\t\t\tif (strip.scrollWidth <= strip.clientWidth || event.deltaX !== 0 || event.deltaY === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tstrip.scrollLeft += event.deltaY\n\t\t}\n\n\t\tprovide(CN_TABS_INJECTION_KEY, { register, unregister, select, isActive })\n\n\t\treturn {\n\t\t\tboardStrip,\n\t\t\tboardClass,\n\t\t\ttabs,\n\t\t\tvisibleTabs,\n\t\t\toverflowTabs,\n\t\t\thasCount,\n\t\t\toverflowLabel,\n\t\t\tonOverflowPick,\n\t\t\tmoreAriaLabel,\n\t\t\tisActive,\n\t\t\tactiveIndex,\n\t\t\tnav,\n\t\t\tnavButtons,\n\t\t\tmoreStart,\n\t\t\tmoreEnd,\n\t\t\tmeasureOverflow,\n\t\t\tscrollStrip,\n\t\t\tonNavKeydown,\n\t\t\tonNavWheel,\n\t\t}\n\t},\n})\n</script>\n\n<style scoped>\n/* The bar owns the rule under the strip so a `#nav-end` surface sits ON the\n line rather than above a line that stops where the last tab does. With no\n nav-end content the nav is the bar's only child and the result is pixel\n identical to the rule living on the nav itself. */\n.cn-tabs__bar {\n\t/* Top-aligned so the `#nav-end` control sits on the tab row, not below it,\n\t should the two sides ever differ in height. */\n\talign-items: flex-start;\n\tborder-bottom: 1px solid var(--color-border);\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n/* The strip holds the scrolling nav and the edge affordances drawn over it. */\n.cn-tabs__strip {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tposition: relative;\n}\n\n.cn-tabs__nav {\n\tdisplay: flex;\n\tgap: 4px;\n\t/* One row that scrolls, never a wrap: in a narrow sidebar a nine-tab strip\n\t wrapped into a stack taller than the panel under it. The scrollbar is\n\t hidden because it would sit between the tabs and the bar's rule and break\n\t the open tab's join with its panel; a fade and a chevron on the clipped\n\t edge say there is more, and the strip follows the wheel. */\n\tflex-wrap: nowrap;\n\toverflow-x: auto;\n\toverflow-y: hidden;\n\tscroll-behavior: smooth;\n\tscrollbar-width: none;\n\t/* Overhang the bar's 1px rule so the open tab's own bottom edge, drawn in\n\t the panel's colour, covers it and the tab joins its panel. */\n\tmargin-bottom: -1px;\n}\n\n.cn-tabs__nav::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cn-tabs__nav {\n\t\tscroll-behavior: auto;\n\t}\n}\n\n/* A fade over the clipped edge, only on the side that hides tabs. Stops a\n pixel above the bar's rule so the rule keeps its full colour.\n\n z-index 2 so the fades outrank a lifted tab: CnTabsWidget puts the open tab\n at 1, and a tie goes to DOM order, which would draw that tab over the start\n fade (it precedes the tabs) while the end fade still covered it. */\n.cn-tabs__strip::before,\n.cn-tabs__strip::after {\n\tcontent: '';\n\tdisplay: none;\n\tinset-block: 0 1px;\n\tpointer-events: none;\n\tposition: absolute;\n\twidth: 56px;\n\tz-index: 2;\n}\n\n.cn-tabs__strip::before {\n\tbackground: linear-gradient(to right, var(--color-main-background) 30%, transparent);\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__strip::after {\n\tbackground: linear-gradient(to left, var(--color-main-background) 30%, transparent);\n\tinset-inline-end: 0;\n}\n\n.cn-tabs__strip--more-start::before,\n.cn-tabs__strip--more-end::after {\n\tdisplay: block;\n}\n\n/* Doubled selector for the same reason as `.cn-tabs__nav .cn-tabs__nav-item`:\n Nextcloud's own `button` rule would otherwise give this a border, a fill\n and a margin. */\n.cn-tabs__strip .cn-tabs__scroll {\n\talign-items: center;\n\tbackground: none;\n\tborder: 0;\n\tborder-radius: 0;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tdisplay: flex;\n\t/* Pushed down a little so the chevron sits level with the tab labels. */\n\tinset-block: 2px 0;\n\tjustify-content: center;\n\tmargin: 0;\n\tmin-height: 0;\n\tpadding: 0;\n\tposition: absolute;\n\twidth: 28px;\n\tz-index: 3;\n}\n\n/* Nextcloud's `button:hover` rule would otherwise fill the chevron. */\n.cn-tabs__strip .cn-tabs__scroll:hover,\n.cn-tabs__strip .cn-tabs__scroll:focus {\n\tbackground: none;\n}\n\n.cn-tabs__scroll--start {\n\tinset-inline-start: 0;\n}\n\n.cn-tabs__scroll--end {\n\tinset-inline-end: 0;\n}\n\n/* The count behind a tab's title. Full-contrast text on the page background,\n so it reads on the idle tab's darker fill and on the open tab alike. */\n.cn-tabs__count {\n\tbackground-color: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-pill, 100px);\n\tcolor: var(--color-main-text);\n\tdisplay: inline-block;\n\tfont-size: 0.85em;\n\tline-height: 1.4;\n\tmargin-inline-start: 6px;\n\tmin-width: 1.4em;\n\tpadding: 0 6px;\n\ttext-align: center;\n}\n\n.cn-tabs__more {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: 0 0 auto;\n}\n\n/* Keeps a \"More\" entry's text on the menu's text column without drawing an icon. */\n.cn-tabs__more-spacer {\n\tdisplay: inline-block;\n\twidth: 20px;\n}\n\n.cn-tabs__nav-end {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\tgap: 4px;\n\t/* No min-height. This used to hard-code 38px to mirror the old nav item's\n\t vertical box, but the folder-tab restyle changed that box to 33px, and\n\t because the bar is `align-items: flex-start` the taller nav-end then drove\n\t the BAR's height. The bar's bottom rule sank 6px below the tab row, so the\n\t open tab no longer met the panel it is drawn as joined to. Letting the\n\t control size to its own content keeps the rule on the tab row whatever the\n\t nav item's box becomes, so the two cannot drift apart again. */\n\tmin-height: 0;\n}\n\n/* Folder tabs, not underlined labels. An underline is the weakest possible\n affordance on a strip that already carries icons: with nine tabs on a dossiq\n case the row read as a sentence of links, and only the 2px rule said which\n one was open. Giving the inactive tabs their own darker surface makes the\n strip legible as a control at a glance, and the active tab, sharing the\n panel's background and punching a hole in the bar's rule, reads as the sheet\n in front. */\n.cn-tabs__nav-item {\n\tbackground-color: var(--color-background-dark);\n\t/* `--color-border-dark`, not `--color-border`: the latter is the same\n\t #ededed as `--color-background-dark` in the stock light theme, so a tab\n\t drawn with it has no visible edge against its own fill. */\n\tborder: 1px solid var(--color-border-dark);\n\tborder-bottom: none;\n\tborder-radius: var(--border-radius-large, 8px) var(--border-radius-large, 8px) 0 0;\n\t/* Full-contrast label, not `--color-text-maxcontrast`. On the old\n\t transparent strip maxcontrast sat on white at 5.3:1; on this darker\n\t surface the same pair measures 4.55:1, which clears WCAG AA by 0.05 and\n\t would not survive a themed instance shifting either token. A real tab\n\t strip carries its selection in the surface and the weight anyway, so\n\t nothing is lost by giving every label the readable colour. */\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tfont-weight: normal;\n\tpadding: 8px 12px;\n\twhite-space: nowrap;\n\t/* Match the `#nav-end` control's own height. The bar is a flex row, so\n\t whichever side is taller sets the bar's height and therefore where its\n\t bottom rule lands. When the control was the taller one the rule sat below\n\t the tab row and showed as a line under the open tab, breaking the join to\n\t the panel. Sizing both sides the same is what keeps the rule ON the tab\n\t row; `inline-flex` centres the label inside the taller box. */\n\talign-items: center;\n\tbox-sizing: border-box;\n\tdisplay: inline-flex;\n\tmin-height: var(--default-clickable-area, 34px);\n}\n\n/* Nextcloud's server stylesheet gives every plain `button` a 3px bottom margin\n with a selector that outscores the scoped rule above, which floated the tabs\n above the bar's rule inside a real page. The parent class wins on\n specificity rather than on `!important` or source order. */\n.cn-tabs__nav .cn-tabs__nav-item {\n\tmargin-block-end: 0;\n}\n\n.cn-tabs__nav--justified .cn-tabs__nav-item {\n\tflex: 1 1 0;\n}\n\n.cn-tabs__nav-item:hover,\n.cn-tabs__nav-item:focus-visible {\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n/* Inside the box: the scrolling strip clips anything drawn outside a tab. */\n.cn-tabs__nav-item:focus-visible {\n\toutline-offset: -2px;\n}\n\n.cn-tabs__nav-item[disabled] {\n\tcursor: default;\n\topacity: 0.5;\n}\n\n.cn-tabs__nav-item[disabled]:hover {\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n}\n\n/* The open tab: the panel's own background, and a bottom border painted in\n that same background so it erases the bar's rule underneath and the tab\n joins the sheet below it. The primary-coloured top edge is what carries the\n selection at a glance, and it survives forced-colours mode, which drops\n background colours but keeps borders. */\n.cn-tabs__nav-item--active {\n\tbackground-color: var(--color-main-background);\n\tborder-bottom: 1px solid var(--color-main-background);\n\tborder-top: 2px solid var(--color-primary-element);\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n\t/* Keep the text baseline identical to an inactive tab despite the 1px\n\t thicker top border, so the strip does not jog when the selection moves. */\n\tpadding-top: 7px;\n}\n\n.cn-tabs__content {\n\tpadding-top: 12px;\n}\n\n.cn-tabs--card .cn-tabs__content {\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\tpadding: 12px;\n}\n\n/* Segmented variant: the strip is a pill switch that stands on its own, so\n the bar's rule and the tab-joins-panel treatment are both dropped. */\n.cn-tabs--segmented .cn-tabs__bar {\n\tborder-bottom: none;\n}\n\n.cn-tabs--segmented .cn-tabs__strip {\n\tflex: 0 1 auto;\n}\n\n.cn-tabs--segmented .cn-tabs__nav {\n\tgap: 4px;\n\tmargin-bottom: 0;\n\tpadding: 4px;\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 10px));\n\tbackground-color: var(--color-background-dark);\n}\n\n.cn-tabs--segmented .cn-tabs__nav-item {\n\tborder: 1px solid transparent;\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tbackground-color: transparent;\n\tfont-weight: 600;\n\tpadding: 6px 16px;\n}\n\n.cn-tabs--segmented .cn-tabs__nav-item:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n/* The open segment is the raised one. Its border survives forced-colours\n mode, which drops the background and the shadow. */\n.cn-tabs--segmented .cn-tabs__nav-item--active {\n\tborder: 1px solid var(--color-border-dark);\n\tbackground-color: var(--color-main-background);\n\tbox-shadow: 0 1px 2px var(--color-box-shadow);\n\tfont-weight: 600;\n\tpadding-top: 6px;\n}\n\n/* Underline variant (board LpBeheerAlgemeen): plain labels on the bar's rule,\n the open one carried by a 3px primary underline that sits on that rule. The\n strip wraps onto a second row rather than scrolling. */\n.cn-tabs--underline .cn-tabs__nav {\n\tflex-wrap: wrap;\n\toverflow: visible;\n}\n\n.cn-tabs--underline .cn-tabs__nav-item {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-bottom: 3px solid transparent;\n\tborder-radius: 0;\n\tcolor: var(--cn-board-text-soft, var(--color-main-text));\n\tfont-size: 14px;\n\tfont-weight: 500;\n\tgap: 8px;\n\tmin-height: 44px;\n\tpadding: 0 9px;\n}\n\n.cn-tabs--underline .cn-tabs__nav-item:hover,\n.cn-tabs--underline .cn-tabs__nav-item:focus-visible {\n\tbackground-color: transparent;\n\tborder-bottom-color: var(--color-border-dark);\n\tcolor: var(--color-main-text);\n}\n\n.cn-tabs--underline .cn-tabs__nav-item[disabled]:hover {\n\tbackground-color: transparent;\n\tborder-bottom-color: transparent;\n}\n\n.cn-tabs--underline .cn-tabs__nav-item--active,\n.cn-tabs--underline .cn-tabs__nav-item--active:hover,\n.cn-tabs--underline .cn-tabs__nav-item--active:focus-visible {\n\tborder-bottom: 3px solid var(--color-primary-element);\n\tcolor: var(--color-main-text);\n\tfont-weight: 700;\n\tpadding-top: 0;\n}\n\n.cn-tabs--underline .cn-tabs__count {\n\tmargin-inline-start: 0;\n}\n\n.cn-tabs--underline .cn-tabs__content {\n\tpadding-top: 20px;\n}\n</style>\n"],"names":["t"],"mappings":"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"}