@salesforce/templates 66.17.4 → 66.18.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 (328) hide show
  1. package/lib/generators/analyticsTemplateGenerator.d.ts +1 -1
  2. package/lib/generators/analyticsTemplateGenerator.js +1 -1
  3. package/lib/generators/analyticsTemplateGenerator.js.map +1 -1
  4. package/lib/generators/apexClassGenerator.d.ts +1 -1
  5. package/lib/generators/apexClassGenerator.js +1 -0
  6. package/lib/generators/apexClassGenerator.js.map +1 -1
  7. package/lib/generators/apexTriggerGenerator.d.ts +1 -1
  8. package/lib/generators/baseGenerator.d.ts +1 -1
  9. package/lib/generators/baseGenerator.js +13 -7
  10. package/lib/generators/baseGenerator.js.map +1 -1
  11. package/lib/generators/digitalExperienceSiteGenerator.d.ts +1 -2
  12. package/lib/generators/digitalExperienceSiteGenerator.js +9 -9
  13. package/lib/generators/digitalExperienceSiteGenerator.js.map +1 -1
  14. package/lib/generators/flexipageGenerator.d.ts +1 -2
  15. package/lib/generators/flexipageGenerator.js +5 -7
  16. package/lib/generators/flexipageGenerator.js.map +1 -1
  17. package/lib/generators/lightningAppGenerator.d.ts +1 -1
  18. package/lib/generators/lightningComponentGenerator.d.ts +1 -1
  19. package/lib/generators/lightningEventGenerator.d.ts +1 -1
  20. package/lib/generators/lightningInterfaceGenerator.d.ts +1 -1
  21. package/lib/generators/lightningOutGenerator.d.ts +1 -1
  22. package/lib/generators/lightningOutGenerator.js +21 -5
  23. package/lib/generators/lightningOutGenerator.js.map +1 -1
  24. package/lib/generators/lightningTestGenerator.d.ts +1 -1
  25. package/lib/generators/lightningTestGenerator.js +1 -1
  26. package/lib/generators/lightningTestGenerator.js.map +1 -1
  27. package/lib/generators/projectGenerator.d.ts +1 -1
  28. package/lib/generators/projectGenerator.js +8 -5
  29. package/lib/generators/projectGenerator.js.map +1 -1
  30. package/lib/generators/staticResourceGenerator.d.ts +1 -1
  31. package/lib/generators/uiBundleGenerator.d.ts +1 -1
  32. package/lib/generators/uiBundleGenerator.js +4 -2
  33. package/lib/generators/uiBundleGenerator.js.map +1 -1
  34. package/lib/generators/uiEmbeddingGenerator.d.ts +1 -1
  35. package/lib/generators/uiEmbeddingGenerator.js +2 -1
  36. package/lib/generators/uiEmbeddingGenerator.js.map +1 -1
  37. package/lib/generators/visualforceComponentGenerator.d.ts +1 -1
  38. package/lib/generators/visualforcePageGenerator.d.ts +1 -1
  39. package/lib/i18n/index.js +1 -1
  40. package/lib/i18n/index.js.map +1 -1
  41. package/lib/i18n/localization.js +13 -12
  42. package/lib/i18n/localization.js.map +1 -1
  43. package/lib/index.js.map +1 -1
  44. package/lib/service/gitRepoUtils.d.ts +1 -1
  45. package/lib/service/gitRepoUtils.js +7 -8
  46. package/lib/service/gitRepoUtils.js.map +1 -1
  47. package/lib/service/templateService.d.ts +1 -1
  48. package/lib/service/templateService.js +1 -1
  49. package/lib/service/templateService.js.map +1 -1
  50. package/lib/templates/project/angularextapp/_p_/_m_/_w_/_a_/package-lock.json +558 -533
  51. package/lib/templates/project/angularintapp/CHANGELOG.md +8 -0
  52. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/eslint.config.js +2 -1
  53. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/package-lock.json +568 -543
  54. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/package.json +4 -4
  55. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/src/app/components/ui/icon/icon.ts +44 -0
  56. package/lib/templates/project/angularintapp/package.json +1 -1
  57. package/lib/templates/project/reactexternalapp/CHANGELOG.md +40 -0
  58. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/eslint.config.js +8 -12
  59. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +362 -344
  60. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +5 -5
  61. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/button.tsx +1 -1
  62. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/sonner.tsx +7 -1
  63. package/lib/templates/project/reactexternalapp/package.json +1 -1
  64. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +363 -345
  65. package/lib/templates/project/vueinternalapp/.forceignore +15 -0
  66. package/lib/templates/project/vueinternalapp/.husky/pre-commit +4 -0
  67. package/lib/templates/project/vueinternalapp/.prettierignore +11 -0
  68. package/lib/templates/project/vueinternalapp/.prettierrc +17 -0
  69. package/lib/templates/project/vueinternalapp/CHANGELOG.md +5375 -0
  70. package/lib/templates/project/vueinternalapp/PRD.md +35 -0
  71. package/lib/templates/project/vueinternalapp/README.md +125 -0
  72. package/lib/templates/project/vueinternalapp/_p_/_m_/_ps_/vueinternalapp_Access.permissionset-meta.xml +13 -0
  73. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/.forceignore +15 -0
  74. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/.graphqlrc.yml +2 -0
  75. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/.prettierignore +9 -0
  76. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/.prettierrc +11 -0
  77. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/README.md +35 -0
  78. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/codegen.yml +95 -0
  79. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/components.json +19 -0
  80. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/e2e/app.spec.ts +17 -0
  81. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/eslint.config.js +124 -0
  82. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/index.html +12 -0
  83. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/package-lock.json +15530 -0
  84. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/package.json +73 -0
  85. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/playwright.config.ts +24 -0
  86. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/scripts/get-graphql-schema.mjs +71 -0
  87. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/scripts/rewrite-e2e-assets.mjs +23 -0
  88. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/App.vue +5 -0
  89. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/api/apiUtils.ts +56 -0
  90. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/api/orgApiVersionService.ts +37 -0
  91. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/api/searchableContentTypesService.ts +119 -0
  92. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/channelResolver.ts +40 -0
  93. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/cmsQueryFragment.ts +108 -0
  94. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/composables/useSearchableContentTypes.ts +132 -0
  95. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/contentTypeSessionCache.ts +134 -0
  96. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/contentTypeUtils.ts +40 -0
  97. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/index.ts +55 -0
  98. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/parseResponse.ts +65 -0
  99. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/searchChannel.ts +31 -0
  100. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/cms/types.ts +66 -0
  101. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/registry.ts +38 -0
  102. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/sobject/index.ts +19 -0
  103. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/sobject/parseResponse.ts +55 -0
  104. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/sobject/queryFragment.ts +141 -0
  105. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/adapters/types.ts +103 -0
  106. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/api/distinctValuesService.ts +81 -0
  107. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/api/searchService.ts +136 -0
  108. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/GlobalSearchBox.types.ts +18 -0
  109. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/GlobalSearchBox.vue +73 -0
  110. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/MergedSearchResults.types.ts +24 -0
  111. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/MergedSearchResults.vue +263 -0
  112. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/Search.vue +200 -0
  113. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/SearchResults.vue +99 -0
  114. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/SourceSection.vue +204 -0
  115. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/PaginationControls.vue +180 -0
  116. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/ScopeSelector.vue +91 -0
  117. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/SearchBar.vue +56 -0
  118. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/SortControl.vue +55 -0
  119. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/ActiveFilters.vue +59 -0
  120. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/DefaultFilterField.vue +63 -0
  121. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/DefaultFilterPanel.vue +31 -0
  122. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/FilterContext.ts +45 -0
  123. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/FilterProvider.vue +23 -0
  124. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/FilterResetButton.vue +17 -0
  125. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/PicklistField.vue +35 -0
  126. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/BooleanFilter.vue +47 -0
  127. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/DateRangeFilter.vue +144 -0
  128. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/FilterFieldWrapper.vue +23 -0
  129. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/MultiSelectFilter.vue +95 -0
  130. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/NumericRangeFilter.vue +117 -0
  131. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/SelectFilter.vue +44 -0
  132. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/TextFilter.vue +49 -0
  133. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/results/CmsResultRow.vue +160 -0
  134. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/results/DefaultResultRow.vue +102 -0
  135. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/results/RenderVNode.ts +10 -0
  136. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/results/resolveResultRenderer.ts +41 -0
  137. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/composables/useAsyncData.ts +66 -0
  138. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/composables/useDistinctValues.ts +67 -0
  139. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/composables/useSearch.ts +906 -0
  140. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/config.json +88 -0
  141. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/constants.ts +8 -0
  142. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/index.ts +115 -0
  143. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/loadConfig.ts +14 -0
  144. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/queryBuilder.ts +100 -0
  145. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/types.ts +375 -0
  146. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/debounce.ts +17 -0
  147. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/fieldUtils.ts +14 -0
  148. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/filterUtils.ts +304 -0
  149. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/sortUtils.ts +34 -0
  150. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/getAccountDetail.graphql +121 -0
  151. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +44 -0
  152. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/appLayout.vue +153 -0
  153. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.vue +76 -0
  154. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/AccountDetailContent.vue +124 -0
  155. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/AccountDetailError.vue +28 -0
  156. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/AccountDetailNotFound.vue +22 -0
  157. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/AccountDetailSkeleton.vue +57 -0
  158. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/DetailSection.vue +25 -0
  159. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/FieldItem.vue +17 -0
  160. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/FieldRow.vue +5 -0
  161. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/SkeletonFieldRow.vue +12 -0
  162. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/TelephoneField.vue +7 -0
  163. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/account/types.ts +42 -0
  164. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.vue +59 -0
  165. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/layouts/card-layout.vue +30 -0
  166. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/alert/Alert.vue +21 -0
  167. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/alert/AlertAction.vue +17 -0
  168. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/alert/AlertDescription.vue +22 -0
  169. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/alert/AlertTitle.vue +22 -0
  170. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/alert/index.ts +25 -0
  171. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/avatar/Avatar.vue +23 -0
  172. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/avatar/AvatarFallback.vue +31 -0
  173. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/avatar/AvatarImage.vue +16 -0
  174. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/avatar/index.ts +3 -0
  175. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/button/Button.vue +33 -0
  176. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/button/index.ts +43 -0
  177. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/Card.vue +29 -0
  178. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/CardAction.vue +22 -0
  179. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/CardContent.vue +17 -0
  180. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/CardDescription.vue +17 -0
  181. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/CardFooter.vue +22 -0
  182. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/CardHeader.vue +22 -0
  183. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/CardTitle.vue +22 -0
  184. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/card/index.ts +7 -0
  185. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/checkbox/Checkbox.vue +43 -0
  186. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/checkbox/index.ts +1 -0
  187. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/collapsible/Collapsible.vue +19 -0
  188. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/collapsible/CollapsibleContent.vue +12 -0
  189. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  190. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/collapsible/index.ts +3 -0
  191. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/Dialog.vue +15 -0
  192. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogClose.vue +12 -0
  193. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogContent.vue +59 -0
  194. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogDescription.vue +31 -0
  195. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogFooter.vue +33 -0
  196. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogHeader.vue +17 -0
  197. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogOverlay.vue +31 -0
  198. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogPortal.vue +12 -0
  199. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogTitle.vue +26 -0
  200. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/DialogTrigger.vue +12 -0
  201. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dialog/index.ts +10 -0
  202. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  203. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +51 -0
  204. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
  205. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  206. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuItem.vue +44 -0
  207. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +30 -0
  208. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuPortal.vue +12 -0
  209. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  210. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +52 -0
  211. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +26 -0
  212. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
  213. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  214. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +37 -0
  215. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +38 -0
  216. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  217. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/dropdown-menu/index.ts +15 -0
  218. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/Field.vue +27 -0
  219. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldContent.vue +22 -0
  220. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldDescription.vue +24 -0
  221. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldError.vue +48 -0
  222. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldGroup.vue +22 -0
  223. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldLabel.vue +24 -0
  224. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldLegend.vue +29 -0
  225. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldSeparator.vue +31 -0
  226. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldSet.vue +22 -0
  227. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/FieldTitle.vue +22 -0
  228. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/field/index.ts +33 -0
  229. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/index.ts +115 -0
  230. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/input/Input.vue +30 -0
  231. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/input/index.ts +1 -0
  232. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/label/Label.vue +29 -0
  233. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/label/index.ts +1 -0
  234. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/Pagination.vue +17 -0
  235. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/PaginationContent.vue +15 -0
  236. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/PaginationEllipsis.vue +23 -0
  237. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/PaginationItem.vue +7 -0
  238. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/PaginationLink.vue +31 -0
  239. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/PaginationNext.vue +29 -0
  240. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/PaginationPrevious.vue +29 -0
  241. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/pagination/index.ts +7 -0
  242. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/Popover.vue +15 -0
  243. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/PopoverAnchor.vue +12 -0
  244. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/PopoverContent.vue +44 -0
  245. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/PopoverDescription.vue +17 -0
  246. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/PopoverHeader.vue +17 -0
  247. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/PopoverTitle.vue +14 -0
  248. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/PopoverTrigger.vue +12 -0
  249. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/popover/index.ts +7 -0
  250. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/Select.vue +15 -0
  251. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectContent.vue +65 -0
  252. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectGroup.vue +26 -0
  253. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectItem.vue +41 -0
  254. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectLabel.vue +26 -0
  255. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectScrollDownButton.vue +32 -0
  256. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectScrollUpButton.vue +32 -0
  257. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectSeparator.vue +24 -0
  258. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectTrigger.vue +45 -0
  259. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/SelectValue.vue +12 -0
  260. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/select/index.ts +10 -0
  261. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/separator/Separator.vue +33 -0
  262. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/separator/index.ts +1 -0
  263. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/skeleton/Skeleton.vue +15 -0
  264. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/skeleton/index.ts +1 -0
  265. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/sonner/Sonner.vue +29 -0
  266. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/sonner/index.ts +3 -0
  267. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/spinner/Spinner.vue +17 -0
  268. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/components/ui/spinner/index.ts +1 -0
  269. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/composables/useAsyncData.ts +69 -0
  270. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/index.ts +10 -0
  271. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/lib/utils.ts +6 -0
  272. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/main.ts +6 -0
  273. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.vue +98 -0
  274. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/pages/Home.vue +20 -0
  275. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/pages/NotFound.vue +28 -0
  276. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/pages/SearchPage.vue +17 -0
  277. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/router-utils.ts +32 -0
  278. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/router.ts +51 -0
  279. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/styles/global.css +134 -0
  280. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/test/setup.ts +1 -0
  281. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +54 -0
  282. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/types/globals.d.ts +13 -0
  283. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/src/utils/accountFields.ts +25 -0
  284. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/tsconfig.json +45 -0
  285. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/tsconfig.node.json +13 -0
  286. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/tsconfig.tsbuildinfo +1 -0
  287. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/ui-bundle.json +7 -0
  288. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +14 -0
  289. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/vite.config.ts +106 -0
  290. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/vitest-env.d.ts +2 -0
  291. package/lib/templates/project/vueinternalapp/_p_/_m_/_w_/_a_/vueinternalapp.uibundle-meta.xml +8 -0
  292. package/lib/templates/project/vueinternalapp/_p_/_m_/applications/vueinternalapp.app-meta.xml +17 -0
  293. package/lib/templates/project/vueinternalapp/config/project-scratch-def.json +13 -0
  294. package/lib/templates/project/vueinternalapp/eslint.config.js +7 -0
  295. package/lib/templates/project/vueinternalapp/jest.config.js +6 -0
  296. package/lib/templates/project/vueinternalapp/package.json +46 -0
  297. package/lib/templates/project/vueinternalapp/scripts/apex/hello.apex +10 -0
  298. package/lib/templates/project/vueinternalapp/scripts/gitignore-templates.json +4 -0
  299. package/lib/templates/project/vueinternalapp/scripts/graphql-search.sh +191 -0
  300. package/lib/templates/project/vueinternalapp/scripts/network-senders.mjs +84 -0
  301. package/lib/templates/project/vueinternalapp/scripts/org-setup-config-schema.mjs +132 -0
  302. package/lib/templates/project/vueinternalapp/scripts/org-setup-dev.mjs +114 -0
  303. package/lib/templates/project/vueinternalapp/scripts/org-setup-url.mjs +108 -0
  304. package/lib/templates/project/vueinternalapp/scripts/org-setup-utils.mjs +569 -0
  305. package/lib/templates/project/vueinternalapp/scripts/org-setup-xml.mjs +283 -0
  306. package/lib/templates/project/vueinternalapp/scripts/org-setup.config.json +9 -0
  307. package/lib/templates/project/vueinternalapp/scripts/org-setup.mjs +2545 -0
  308. package/lib/templates/project/vueinternalapp/scripts/sf-project-setup.mjs +103 -0
  309. package/lib/templates/project/vueinternalapp/scripts/soql/account.soql +6 -0
  310. package/lib/templates/project/vueinternalapp/scripts/validate-org-setup-config.mjs +38 -0
  311. package/lib/templates/project/vueinternalapp/sfdx-project.json +12 -0
  312. package/lib/templates/uiBundles/angularbasic/package-lock.json +558 -533
  313. package/lib/templates/uiBundles/reactbasic/eslint.config.js +8 -12
  314. package/lib/templates/uiBundles/reactbasic/package-lock.json +362 -344
  315. package/lib/templates/uiBundles/reactbasic/package.json +5 -5
  316. package/lib/templates/uiBundles/reactbasic/src/components/ui/button.tsx +1 -1
  317. package/lib/templates/uiBundles/reactbasic/src/components/ui/sonner.tsx +7 -1
  318. package/lib/templates/uiBundles/vuebasic/eslint.config.js +6 -1
  319. package/lib/templates/uiBundles/vuebasic/package-lock.json +339 -321
  320. package/lib/templates/uiBundles/vuebasic/package.json +5 -5
  321. package/lib/utils/createUtil.js +1 -1
  322. package/lib/utils/createUtil.js.map +1 -1
  323. package/lib/utils/types.d.ts +8 -7
  324. package/lib/utils/types.js +4 -4
  325. package/lib/utils/types.js.map +1 -1
  326. package/lib/utils/uiBundleTemplateUtils.js +13 -9
  327. package/lib/utils/uiBundleTemplateUtils.js.map +1 -1
  328. package/package.json +10 -12
@@ -0,0 +1,906 @@
1
+ /**
2
+ * useSearch — Vue composable orchestrating configuration-driven multi-source search.
3
+ *
4
+ * Owns:
5
+ * - global `q` (one search box drives all sources)
6
+ * - per-source filters / sort / pagination (independent state per source)
7
+ * - URL sync (debounced; namespace `s.<key>.f.<field>=...`)
8
+ * - one batched GraphQL fetch on every state change
9
+ *
10
+ * Returns a per-source `controller` map plus aggregate loading/error flags.
11
+ * Rendering and routing are the caller's responsibility.
12
+ */
13
+
14
+ import { computed, getCurrentScope, onScopeDispose, reactive, ref, watch } from "vue";
15
+ import { useRoute, useRouter, type LocationQuery } from "vue-router";
16
+ import { runSearch } from "../api/searchService";
17
+ import {
18
+ GLOBAL_QUERY_KEY,
19
+ readSourceParams,
20
+ writeSourceParams,
21
+ type ActiveFilterValue,
22
+ } from "../utils/filterUtils";
23
+ import type { SortState } from "../utils/sortUtils";
24
+ import { debounce } from "../utils/debounce";
25
+ import type {
26
+ SourceConfig,
27
+ SourceController,
28
+ SourceResult,
29
+ SearchConfig,
30
+ SearchHandle,
31
+ SearchScope,
32
+ GlobalPagination,
33
+ MergedResultItem,
34
+ } from "../types";
35
+ import { ALL_SCOPE, parseScope } from "../types";
36
+ import type { SourceRequest } from "../adapters/types";
37
+ import { MIN_QUERY_LENGTH } from "../constants";
38
+ // `getUIBundleId` supplies the search-query UIBundle id (the isolated swap
39
+ // point) and is resolved by this caller BEFORE the sync query builder runs;
40
+ // `resolveChannelId` derives the discovery channel id from a CMS result;
41
+ // `useSearchableContentTypes` is the session-cached discovery hook;
42
+ // `isValidCmsFqn` validates a selected FQN before it enters state or a query.
43
+ import {
44
+ getUIBundleId,
45
+ getUserLanguage,
46
+ isConfiguredUIBundleId,
47
+ } from "../adapters/cms/searchChannel";
48
+ import { getOrgSupportsCmsSearch } from "../adapters/cms/api/orgApiVersionService";
49
+ import { resolveChannelId } from "../adapters/cms/channelResolver";
50
+ import { useSearchableContentTypes } from "../adapters/cms/composables/useSearchableContentTypes";
51
+ import { isValidCmsFqn } from "../adapters/cms/contentTypeUtils";
52
+ import { readLastChannelId, writeLastChannelId } from "../adapters/cms/contentTypeSessionCache";
53
+
54
+ const URL_SYNC_DEBOUNCE_MS = 300;
55
+
56
+ /**
57
+ * Returns true when `source` should participate in the current scope. Only the
58
+ * scope's `base` (`"all"` or a source key) decides membership — a
59
+ * `"<cmsKey>:<fqn>"` scope has `base === <cmsKey>`, so it naturally includes ONLY
60
+ * that CMS source and drops every SObject source (CMS-only search).
61
+ */
62
+ function isSourceInScope(scope: SearchScope, sourceKey: string): boolean {
63
+ const { base } = parseScope(scope);
64
+ return base === ALL_SCOPE || base === sourceKey;
65
+ }
66
+
67
+ /**
68
+ * Validates a raw scope string against the config. Accepts `"all"`, any source
69
+ * key, and the `"<cmsKey>:<fqn>"` form (base must be a CMS source and the FQN
70
+ * must pass `isValidCmsFqn`). Returns a safe scope, falling back to `"all"`.
71
+ */
72
+ function validateScope(config: SearchConfig, raw: string): SearchScope {
73
+ if (raw === ALL_SCOPE) return ALL_SCOPE;
74
+ const { base, cmsFqn } = parseScope(raw);
75
+ const source = config.sources.find((s) => s.key === base);
76
+ if (!source) return ALL_SCOPE;
77
+ if (cmsFqn !== undefined) {
78
+ // A content-type scope is only valid on a CMS source with a well-formed FQN.
79
+ return source.kind === "cms" && isValidCmsFqn(cmsFqn) ? raw : ALL_SCOPE;
80
+ }
81
+ return raw;
82
+ }
83
+
84
+ interface SourceLocalState {
85
+ filters: ActiveFilterValue[];
86
+ sort: SortState | null;
87
+ pageSize: number;
88
+ pageIndex: number;
89
+ afterCursor: string | undefined;
90
+ cursorStack: string[];
91
+ }
92
+
93
+ /** Defaults applied when `config.pagination` is omitted. */
94
+ const DEFAULT_PAGE_SIZE = 10;
95
+ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50];
96
+
97
+ interface ResolvedPagination {
98
+ mode: "per-source" | "merged";
99
+ mergeOrder: "sequential" | "interleaved" | "proportional";
100
+ pageSize: number;
101
+ pageSizeOptions: number[];
102
+ }
103
+
104
+ /** Resolves the single global pagination config, filling in defaults. */
105
+ export function resolvePagination(config: SearchConfig): ResolvedPagination {
106
+ const p = config.pagination;
107
+ const pageSizeOptions =
108
+ p?.pageSizeOptions && p.pageSizeOptions.length > 0
109
+ ? p.pageSizeOptions
110
+ : DEFAULT_PAGE_SIZE_OPTIONS;
111
+ return {
112
+ mode: p?.mode ?? "per-source",
113
+ mergeOrder: p?.mergeOrder ?? "sequential",
114
+ pageSize: p?.pageSize ?? pageSizeOptions[0] ?? DEFAULT_PAGE_SIZE,
115
+ pageSizeOptions,
116
+ };
117
+ }
118
+
119
+ /** Clamps a requested size to the configured options. */
120
+ function validatePageSize(pagination: ResolvedPagination, size: number): number {
121
+ const valid = pagination.pageSizeOptions;
122
+ if (valid.length === 0) return size;
123
+ return valid.includes(size) ? size : pagination.pageSize;
124
+ }
125
+
126
+ export function initSourceState(
127
+ source: SourceConfig,
128
+ params: URLSearchParams,
129
+ pagination: ResolvedPagination,
130
+ ): SourceLocalState {
131
+ // `filterBy` / `defaultSort` are SObject-only — a CMS source has neither, so
132
+ // it seeds with no filters and no default sort.
133
+ const filterBy = source.kind === "sobject" ? (source.filterBy ?? []) : [];
134
+ const defaultSort = source.kind === "sobject" ? (source.defaultSort ?? null) : null;
135
+ const read = readSourceParams(params, source.key, filterBy);
136
+ const sort = read.sort ?? defaultSort;
137
+ const pageSize = validatePageSize(pagination, read.pageSize ?? pagination.pageSize);
138
+ return {
139
+ filters: read.filters,
140
+ sort,
141
+ pageSize,
142
+ // Cursors aren't persisted to the URL, so always seed the first page (page param ignored on read).
143
+ pageIndex: 0,
144
+ afterCursor: undefined,
145
+ cursorStack: [],
146
+ };
147
+ }
148
+
149
+ export interface UseSearchOptions {
150
+ /**
151
+ * Lock the search to a single source key (or "all"). When set:
152
+ * - `scope` is forced to this value and `setScope` becomes a no-op.
153
+ * - The URL never writes `?scope=` (it's implicit in the page route).
154
+ * - The dropdown should be hidden by the caller.
155
+ *
156
+ * Use when you embed the search inside a page that already represents
157
+ * the source (e.g. `/accounts/search` rendering only Accounts).
158
+ */
159
+ lockedScope?: SearchScope;
160
+ }
161
+
162
+ /** Flattens a vue-router query object into URLSearchParams (repeated keys preserved). */
163
+ function queryToParams(query: LocationQuery): URLSearchParams {
164
+ const params = new URLSearchParams();
165
+ for (const [key, value] of Object.entries(query)) {
166
+ const values = Array.isArray(value) ? value : [value];
167
+ for (const v of values) if (v != null) params.append(key, v);
168
+ }
169
+ return params;
170
+ }
171
+
172
+ export function useSearch(config: SearchConfig, options?: UseSearchOptions): SearchHandle {
173
+ const route = useRoute();
174
+ const router = useRouter();
175
+ const lockedScope = options?.lockedScope;
176
+
177
+ // Single global pagination config — page size, options, mode, merge order.
178
+ // There is no per-source pagination; this applies to every scope, including
179
+ // locked single-object pages and dropdown-narrowed scope. The config is
180
+ // stable for the composable's lifetime.
181
+ const pagination = resolvePagination(config);
182
+
183
+ // One-time seed from URL.
184
+ const searchParams = queryToParams(route.query);
185
+ const initialSourceStates: Record<string, SourceLocalState> = {};
186
+ for (const source of config.sources) {
187
+ initialSourceStates[source.key] = initSourceState(source, searchParams, pagination);
188
+ }
189
+ const initialQ = searchParams.get(GLOBAL_QUERY_KEY) ?? "";
190
+ // When the caller locks the scope, that always wins. A locked scope is
191
+ // always a plain source key (or "all") — never a content-type form.
192
+ // Otherwise a fresh load always starts in "All" scope. We deliberately do NOT
193
+ // restore a persisted `?scope=` — a narrowed scope (especially a CMS
194
+ // content-type scope) depends on runtime-discovered content types that
195
+ // aren't known yet on first paint, so restoring it would show a blank or
196
+ // orphaned selection.
197
+ const initialScope: SearchScope =
198
+ lockedScope !== undefined && lockedScope !== ALL_SCOPE
199
+ ? config.sources.some((s) => s.key === lockedScope)
200
+ ? lockedScope
201
+ : ALL_SCOPE
202
+ : ALL_SCOPE;
203
+
204
+ const q = ref(initialQ);
205
+ const scope = ref<SearchScope>(initialScope);
206
+ const sourceStates = ref<Record<string, SourceLocalState>>(initialSourceStates);
207
+
208
+ // Merged pagination treats the cumulative result set as one fixed-size-paged
209
+ // list (see SearchPaginationConfig). It needs a single global page index +
210
+ // size rather than the per-source cursors above; per-source state is still
211
+ // kept (for filters / sort) but its cursors stay at page 0.
212
+ const mergedMode = pagination.mode === "merged";
213
+ const mergeOrder = pagination.mergeOrder;
214
+
215
+ const globalPageIndex = ref(0);
216
+ const globalPageSize = ref(pagination.pageSize);
217
+
218
+ let disposed = false;
219
+ if (getCurrentScope()) {
220
+ onScopeDispose(() => {
221
+ disposed = true;
222
+ });
223
+ }
224
+
225
+ // -- URL sync --------------------------------------------------------------
226
+
227
+ const syncToUrl = (
228
+ nextQ: string,
229
+ nextScope: SearchScope,
230
+ nextStates: Record<string, SourceLocalState>,
231
+ ) => {
232
+ // A debounced sync may fire after the host unmounted / navigated away;
233
+ // never write a stale query onto another route.
234
+ if (disposed) return;
235
+ const params = new URLSearchParams();
236
+ if (nextQ) params.set(GLOBAL_QUERY_KEY, nextQ);
237
+ // Intentionally NOT persisting `?scope=`: a fresh load always starts in
238
+ // "All" (see `initialScope`), so writing the scope would leave a stale
239
+ // param the next load ignores. `nextScope` is kept in the signature for
240
+ // call-site compatibility.
241
+ void nextScope;
242
+ for (const source of config.sources) {
243
+ const state = nextStates[source.key];
244
+ if (!state) continue;
245
+ writeSourceParams(
246
+ params,
247
+ source.key,
248
+ state.filters,
249
+ state.sort,
250
+ state.pageSize,
251
+ state.pageIndex,
252
+ // Omit sort / page size from the URL when they equal the
253
+ // defaults, so a reset (or untouched source) leaves a clean
254
+ // query string instead of echoing the defaults. `defaultSort` is
255
+ // SObject-only (CMS has none).
256
+ {
257
+ sort: source.kind === "sobject" ? (source.defaultSort ?? null) : null,
258
+ pageSize: pagination.pageSize,
259
+ },
260
+ );
261
+ }
262
+ const query: Record<string, string> = {};
263
+ params.forEach((value, key) => {
264
+ query[key] = value;
265
+ });
266
+ void router.replace({ query }).catch(() => {
267
+ // Navigation failures (e.g. duplicated navigation) are non-fatal here.
268
+ });
269
+ };
270
+
271
+ const debouncedSync = debounce(syncToUrl, URL_SYNC_DEBOUNCE_MS);
272
+
273
+ const resetCursors = (state: SourceLocalState): SourceLocalState => ({
274
+ ...state,
275
+ pageIndex: 0,
276
+ afterCursor: undefined,
277
+ cursorStack: [],
278
+ });
279
+
280
+ const resetAllCursors = (): Record<string, SourceLocalState> => {
281
+ const next: Record<string, SourceLocalState> = {};
282
+ for (const [key, state] of Object.entries(sourceStates.value)) next[key] = resetCursors(state);
283
+ return next;
284
+ };
285
+
286
+ // Update one source's state and (optionally) reset its pagination.
287
+ const updateSource = (
288
+ sourceKey: string,
289
+ updater: (prev: SourceLocalState) => SourceLocalState,
290
+ resetPagination: boolean,
291
+ ) => {
292
+ // A filter / sort / page-size change reshapes the result set, so the
293
+ // global merged page returns to the first page too.
294
+ if (resetPagination) globalPageIndex.value = 0;
295
+ const prev = sourceStates.value;
296
+ const prior = prev[sourceKey];
297
+ if (!prior) return;
298
+ let next = updater(prior);
299
+ if (resetPagination) next = resetCursors(next);
300
+ const merged = { ...prev, [sourceKey]: next };
301
+ sourceStates.value = merged;
302
+ debouncedSync(q.value, scope.value, merged);
303
+ };
304
+
305
+ // -- Global q --------------------------------------------------------------
306
+
307
+ const setQ = (nextQ: string) => {
308
+ q.value = nextQ;
309
+ // A new term changes the whole result set — back to the first page.
310
+ globalPageIndex.value = 0;
311
+ // Changing the global term invalidates every cursor.
312
+ const next = resetAllCursors();
313
+ sourceStates.value = next;
314
+ debouncedSync(nextQ, scope.value, next);
315
+ };
316
+
317
+ // -- Scope -----------------------------------------------------------------
318
+
319
+ const setScope = (nextScope: SearchScope) => {
320
+ // Locked scope wins over user input — silently no-op.
321
+ if (lockedScope !== undefined) return;
322
+ // Reject unknown scope values; fall back to "all". Accepts a plain
323
+ // source key, "all", or the CMS-only "<cmsKey>:<fqn>" form.
324
+ const validated: SearchScope = validateScope(config, nextScope);
325
+ scope.value = validated;
326
+ // The visible set changes — reset the global page too.
327
+ globalPageIndex.value = 0;
328
+ // Narrowing or widening invalidates every cursor.
329
+ const next = resetAllCursors();
330
+ sourceStates.value = next;
331
+ debouncedSync(q.value, validated, next);
332
+ };
333
+
334
+ // -- Reset all -------------------------------------------------------------
335
+
336
+ const resetAll = () => {
337
+ q.value = "";
338
+ globalPageIndex.value = 0;
339
+ // Preserve locked scope; otherwise widen back to "all".
340
+ const nextScope: SearchScope = lockedScope ?? ALL_SCOPE;
341
+ scope.value = nextScope;
342
+ const empty: Record<string, SourceLocalState> = {};
343
+ for (const source of config.sources) {
344
+ empty[source.key] = {
345
+ filters: [],
346
+ // `defaultSort` is SObject-only; CMS resets to no sort.
347
+ sort: source.kind === "sobject" ? (source.defaultSort ?? null) : null,
348
+ pageSize: pagination.pageSize,
349
+ pageIndex: 0,
350
+ afterCursor: undefined,
351
+ cursorStack: [],
352
+ };
353
+ }
354
+ sourceStates.value = empty;
355
+ globalPageSize.value = pagination.pageSize;
356
+ syncToUrl("", nextScope, empty);
357
+ };
358
+
359
+ // -- CMS content-type discovery --------------------------------------------
360
+ //
361
+ // The channel id used to DISCOVER content types is derived FROM a CMS search
362
+ // result (the permanent path — distinct from the temporary `getUIBundleId()`
363
+ // fed INTO the query). After a CMS result arrives, the fetch watcher resolves
364
+ // it via `resolveChannelId` and stores it here; the session-cached composable
365
+ // then fetches the searchable content types ONCE per channel. The discovered
366
+ // FQNs (a) refine every subsequent all/cms-scope request to `contentTypes` and
367
+ // (b) are exposed for the scope dropdown to append per-type entries.
368
+ // Seeded from the last discovered channel persisted in sessionStorage (keyed
369
+ // by UIBundle id) so a hard reload rehydrates the content-type scope entries.
370
+ const discoveryChannelId = ref<string | null>(null);
371
+ getUIBundleId()
372
+ .then((uiBundleId) => {
373
+ if (disposed || !isConfiguredUIBundleId(uiBundleId)) return;
374
+ const seeded = readLastChannelId(uiBundleId);
375
+ if (seeded && discoveryChannelId.value == null) discoveryChannelId.value = seeded;
376
+ })
377
+ .catch(() => {
378
+ // Seeding is best-effort; a CMS result will still drive discovery.
379
+ });
380
+ const { contentTypes: discoveredContentTypes } = useSearchableContentTypes(discoveryChannelId);
381
+ // The query filter and request key work in FQNs only; the `{ fqn, label }`
382
+ // objects are threaded to the handle for the scope dropdown's display labels.
383
+ const discoveredContentTypeFqns = computed(() =>
384
+ discoveredContentTypes.value.map((ct) => ct.fqn),
385
+ );
386
+
387
+ const requestKey = computed(() => {
388
+ // Stable string key that changes only when something fetch-relevant changes.
389
+ // Sources outside the current scope are listed (with `skip: true`) so
390
+ // the key still changes when scope toggles, but they don't contribute
391
+ // per-source state to the diff.
392
+ return JSON.stringify({
393
+ q: q.value,
394
+ // `scope` already carries the selected CMS content-type fqn (the
395
+ // "<cmsKey>:<fqn>" form), so a content-type selection re-fires the fetch.
396
+ scope: scope.value,
397
+ // Discovered content types are part of the key so that once discovery
398
+ // completes, the watcher re-fires and every in-scope CMS request
399
+ // switches from the bootstrap call to the discovered set.
400
+ ct: discoveredContentTypeFqns.value,
401
+ // Merged mode paginates globally: the global page/size drive the fetch
402
+ // rather than per-source cursors.
403
+ m: mergedMode ? { gp: globalPageIndex.value, gs: globalPageSize.value } : null,
404
+ s: config.sources.map((source) => {
405
+ if (!isSourceInScope(scope.value, source.key)) {
406
+ return { k: source.key, skip: true };
407
+ }
408
+ const st = sourceStates.value[source.key];
409
+ return {
410
+ k: source.key,
411
+ f: st?.filters,
412
+ o: st?.sort,
413
+ // In merged mode per-source size/cursor are overridden below.
414
+ p: mergedMode ? undefined : st?.pageSize,
415
+ a: mergedMode ? undefined : (st?.afterCursor ?? null),
416
+ };
417
+ }),
418
+ });
419
+ });
420
+
421
+ const results = ref<Record<string, SourceResult>>({});
422
+ // Per-source errors, keyed by `source.key`. A source that failed while a
423
+ // sibling succeeded surfaces its own message here without blanking others.
424
+ const sourceErrors = ref<Record<string, string>>({});
425
+ const loading = ref(true);
426
+ // Page-level error — set ONLY when EVERY in-scope source failed, or on a
427
+ // whole-request / network failure (`runSearch` throwing).
428
+ const error = ref<string | null>(null);
429
+ let fetchGen = 0;
430
+
431
+ watch(
432
+ requestKey,
433
+ (_key, _prev, onCleanup) => {
434
+ const generation = ++fetchGen;
435
+ loading.value = true;
436
+ error.value = null;
437
+ sourceErrors.value = {};
438
+
439
+ const currentQ = q.value;
440
+ const currentScope = scope.value;
441
+ const states = sourceStates.value;
442
+ const pageIdx = globalPageIndex.value;
443
+ const pageSz = globalPageSize.value;
444
+ const fqns = discoveredContentTypeFqns.value;
445
+
446
+ const activeSources = config.sources.filter((source) =>
447
+ isSourceInScope(currentScope, source.key),
448
+ );
449
+
450
+ // All sources gated out — clear results and exit without firing a request.
451
+ if (activeSources.length === 0) {
452
+ results.value = {};
453
+ loading.value = false;
454
+ return;
455
+ }
456
+
457
+ // Keyword gate: only fire a search once the user has typed at least
458
+ // MIN_QUERY_LENGTH characters. On landing (or after clearing the box) `q`
459
+ // is empty, so we clear results and exit WITHOUT firing a request — no
460
+ // unfiltered "browse the first page" query, and no CMS bootstrap call
461
+ // until there's a keyword. The SObject and CMS search backends reject
462
+ // terms shorter than MIN_QUERY_LENGTH, which would otherwise surface as
463
+ // "Search failed for this source." across every source.
464
+ if (currentQ.trim().length < MIN_QUERY_LENGTH) {
465
+ results.value = {};
466
+ loading.value = false;
467
+ return;
468
+ }
469
+
470
+ // A "<cmsKey>:<fqn>" scope narrows to one content type. Because its `base`
471
+ // is the CMS source key, `activeSources` already excludes every SObject
472
+ // source (CMS-only search); the fqn flows through as `contentTypes = [fqn]`.
473
+ const { cmsFqn } = parseScope(currentScope);
474
+
475
+ // Merged mode: fetch the whole front-anchored window — (page + 1) * size
476
+ // rows — from every in-scope source, with no cursor. Concatenating these
477
+ // and slicing [page*size, (page+1)*size) yields the exact global page.
478
+ const mergedFetchSize = (pageIdx + 1) * pageSz;
479
+
480
+ // Resolve the async CMS preconditions ONCE, up front, then thread them into
481
+ // each CMS request so the synchronous query builder never resolves a promise.
482
+ let cancelled = false;
483
+ onCleanup(() => {
484
+ cancelled = true;
485
+ });
486
+ // True once this run's request set is a DISCOVERY-driven refetch: an
487
+ // all/cms-scope search that auto-applies the content-type FQNs discovered
488
+ // from a prior successful CMS result. Such a refetch only NARROWS an
489
+ // already-successful bootstrap, so if it fails it must not destroy the
490
+ // good bootstrap results or raise a page-level error.
491
+ let isDiscoveryRefetch = false;
492
+ Promise.all([getUIBundleId(), getUserLanguage(), getOrgSupportsCmsSearch()])
493
+ .then(([uiBundleId, userLanguage, orgSupportsCmsSearch]) => {
494
+ if (cancelled || generation !== fetchGen) return;
495
+
496
+ const requests: SourceRequest[] = activeSources.flatMap((source) => {
497
+ const st = states[source.key]!;
498
+ const base: SourceRequest = {
499
+ source,
500
+ q: currentQ,
501
+ filters: st.filters,
502
+ sort: st.sort,
503
+ pageSize: mergedMode ? mergedFetchSize : st.pageSize,
504
+ afterCursor: mergedMode ? undefined : st.afterCursor,
505
+ };
506
+ if (source.kind !== "cms") return [base];
507
+ // CMS is gated on (a) a CONFIGURED (9YE-shaped) UIBundle id and
508
+ // (b) a build-time API version with the CMS search backend (v68+,
509
+ // core 264+). Skip silently when either is absent.
510
+ if (!isConfiguredUIBundleId(uiBundleId) || !orgSupportsCmsSearch) {
511
+ return [];
512
+ }
513
+ // CMS request: attach the resolved channel id and content types.
514
+ // - specific fqn selected → exactly that type.
515
+ // - all / cms-key scope → the discovered set if known, else
516
+ // omit `contentTypes` entirely (the bootstrap call).
517
+ const contentTypes = cmsFqn ? [cmsFqn] : fqns.length > 0 ? fqns : undefined;
518
+ if (!cmsFqn && fqns.length > 0) {
519
+ isDiscoveryRefetch = true;
520
+ }
521
+ return [{ ...base, uiBundleId, language: userLanguage, contentTypes }];
522
+ });
523
+
524
+ // Every in-scope source was gated out (e.g. a CMS-only scope while CMS
525
+ // is not yet configured): nothing to fetch.
526
+ if (requests.length === 0) {
527
+ results.value = {};
528
+ loading.value = false;
529
+ return;
530
+ }
531
+
532
+ // The sources actually queried (CMS may have been skipped above). Used
533
+ // for the all-failed check so a skipped CMS source is never a failure.
534
+ const requestedSources = requests.map((r) => r.source);
535
+
536
+ return runSearch(requests).then((outcome) => {
537
+ if (cancelled || generation !== fetchGen) return;
538
+ const { results: nextResults, errors: nextErrors } = outcome;
539
+
540
+ // Discovery-driven refetch that came back with a failed source:
541
+ // keep the bootstrap results, fall back to the prior result for
542
+ // any source this refetch failed to produce, drop the error.
543
+ if (isDiscoveryRefetch && requestedSources.some((s) => nextErrors[s.key] != null)) {
544
+ const merged = { ...results.value };
545
+ for (const source of requestedSources) {
546
+ if (nextResults[source.key] != null) merged[source.key] = nextResults[source.key];
547
+ }
548
+ results.value = merged;
549
+ return;
550
+ }
551
+
552
+ results.value = nextResults;
553
+ sourceErrors.value = nextErrors;
554
+
555
+ // Page-level error only when EVERY queried source failed; a mix of
556
+ // success + failure surfaces per-source with the grid intact.
557
+ const allFailed =
558
+ requestedSources.length > 0 &&
559
+ requestedSources.every((source) => nextErrors[source.key] != null);
560
+ if (allFailed) {
561
+ // De-duplicate identical messages so a shared backend error is
562
+ // shown once rather than repeated per source.
563
+ const combined = [
564
+ ...new Set(
565
+ requestedSources
566
+ .map((source) => nextErrors[source.key])
567
+ .filter((m): m is string => !!m),
568
+ ),
569
+ ].join("; ");
570
+ error.value = combined || "Unified search failed";
571
+ }
572
+
573
+ // Content-type discovery: once a CMS source returns data, derive the
574
+ // discovery channel id from it and hand it to the cached composable.
575
+ for (const source of requestedSources) {
576
+ if (source.kind !== "cms") continue;
577
+ const channelId = resolveChannelId(nextResults[source.key]);
578
+ if (channelId) {
579
+ if (discoveryChannelId.value !== channelId) discoveryChannelId.value = channelId;
580
+ // Persist the pointer keyed by THIS UIBundle so a reload seeds
581
+ // only this bundle's channel — never a different bundle's.
582
+ writeLastChannelId(uiBundleId, channelId);
583
+ break;
584
+ }
585
+ }
586
+ });
587
+ })
588
+ .catch((err: unknown) => {
589
+ if (cancelled || generation !== fetchGen) return;
590
+ console.error(err);
591
+ // A failed discovery-driven refetch must NOT blank the good bootstrap
592
+ // results or raise a page-level error.
593
+ if (isDiscoveryRefetch) return;
594
+ // A whole-request / network failure is page-level.
595
+ error.value = err instanceof Error ? err.message : "Unified search failed";
596
+ // Clear stale rows so a prior page's results don't linger under the
597
+ // error banner.
598
+ results.value = {};
599
+ sourceErrors.value = {};
600
+ })
601
+ .finally(() => {
602
+ if (cancelled || generation !== fetchGen) return;
603
+ loading.value = false;
604
+ });
605
+ },
606
+ { immediate: true },
607
+ );
608
+
609
+ // -- Per-source controllers ------------------------------------------------
610
+
611
+ const controllers = computed(() => {
612
+ const out: Record<string, SourceController> = {};
613
+ for (const source of config.sources) {
614
+ const state = sourceStates.value[source.key];
615
+ const result = results.value[source.key] ?? null;
616
+
617
+ const setFilter = (field: string, value: ActiveFilterValue | undefined) => {
618
+ updateSource(
619
+ source.key,
620
+ (prev) => {
621
+ const filtered = prev.filters.filter((f) => f.field !== field);
622
+ if (value) filtered.push(value);
623
+ return { ...prev, filters: filtered };
624
+ },
625
+ true,
626
+ );
627
+ };
628
+ const removeFilter = (field: string) => {
629
+ updateSource(
630
+ source.key,
631
+ (prev) => ({
632
+ ...prev,
633
+ filters: prev.filters.filter((f) => f.field !== field),
634
+ }),
635
+ true,
636
+ );
637
+ };
638
+ const setSort = (sort: SortState | null) => {
639
+ updateSource(source.key, (prev) => ({ ...prev, sort }), true);
640
+ };
641
+ const setPageSize = (size: number) => {
642
+ updateSource(
643
+ source.key,
644
+ (prev) => ({ ...prev, pageSize: validatePageSize(pagination, size) }),
645
+ true,
646
+ );
647
+ };
648
+ const goToNextPage = () => {
649
+ const cursor = result?.pageInfo?.endCursor;
650
+ if (!cursor) return;
651
+ updateSource(
652
+ source.key,
653
+ (prev) => ({
654
+ ...prev,
655
+ cursorStack: [...prev.cursorStack, cursor],
656
+ afterCursor: cursor,
657
+ pageIndex: prev.pageIndex + 1,
658
+ }),
659
+ false,
660
+ );
661
+ };
662
+ const goToPreviousPage = () => {
663
+ updateSource(
664
+ source.key,
665
+ (prev) => {
666
+ if (prev.pageIndex === 0) return prev;
667
+ const nextStack = prev.cursorStack.slice(0, -1);
668
+ return {
669
+ ...prev,
670
+ cursorStack: nextStack,
671
+ afterCursor: nextStack[nextStack.length - 1],
672
+ pageIndex: Math.max(0, prev.pageIndex - 1),
673
+ };
674
+ },
675
+ false,
676
+ );
677
+ };
678
+
679
+ out[source.key] = {
680
+ config: source,
681
+ result,
682
+ loading: loading.value,
683
+ // Per-source error (set only when THIS source failed) — not the
684
+ // page-level `error` (which is set only when every source failed).
685
+ error: sourceErrors.value[source.key] ?? null,
686
+ filters: {
687
+ active: state?.filters ?? [],
688
+ set: setFilter,
689
+ remove: removeFilter,
690
+ },
691
+ sort: {
692
+ current: state?.sort ?? null,
693
+ set: setSort,
694
+ },
695
+ pagination: {
696
+ pageSize: state?.pageSize ?? pagination.pageSize,
697
+ pageSizeOptions: pagination.pageSizeOptions,
698
+ pageIndex: state?.pageIndex ?? 0,
699
+ hasNextPage: result?.pageInfo?.hasNextPage ?? false,
700
+ hasPreviousPage: (state?.pageIndex ?? 0) > 0,
701
+ setPageSize,
702
+ goToNextPage,
703
+ goToPreviousPage,
704
+ },
705
+ };
706
+ }
707
+ return out;
708
+ });
709
+
710
+ const scopeLocked = lockedScope !== undefined;
711
+
712
+ // -- Aggregate (cross-source) view -----------------------------------------
713
+
714
+ const inScopeSources = computed(() =>
715
+ config.sources
716
+ .map((s) => controllers.value[s.key])
717
+ .filter((c): c is SourceController => !!c && isSourceInScope(scope.value, c.config.key)),
718
+ );
719
+
720
+ // Cumulative result count is mode-independent: sum the in-scope totals.
721
+ const totalCount = computed(() =>
722
+ inScopeSources.value.reduce(
723
+ (sum, c) => sum + (c.result?.totalCount ?? c.result?.nodes.length ?? 0),
724
+ 0,
725
+ ),
726
+ );
727
+
728
+ // Every in-scope source's fetched nodes flattened into one list, ordered per
729
+ // `mergeOrder`:
730
+ // - sequential: all of source 1, then source 2, … (config order).
731
+ // - interleaved: round-robin, one node per source per round — equal priority.
732
+ // - proportional: each item gets a key (i + 0.5) / weight (weight = source's
733
+ // totalCount); sorting by key spreads each source evenly across the list
734
+ // in proportion to its size, so larger sources appear more often.
735
+ // All three stay exact in merged mode (each source fetched (page + 1) * size
736
+ // rows from its front, so the first (page + 1) * size items are the true
737
+ // cumulative prefix). Proportional uses the true totalCount as the weight.
738
+ const concatenatedNodes = computed<MergedResultItem[]>(() => {
739
+ const lists = inScopeSources.value.map((c) => ({
740
+ key: c.config.key,
741
+ nodes: c.result?.nodes ?? [],
742
+ weight: c.result?.totalCount ?? c.result?.nodes.length ?? 0,
743
+ }));
744
+ const out: MergedResultItem[] = [];
745
+
746
+ if (mergeOrder === "interleaved") {
747
+ const maxLen = lists.reduce((m, l) => Math.max(m, l.nodes.length), 0);
748
+ for (let round = 0; round < maxLen; round++) {
749
+ for (const l of lists) {
750
+ if (round < l.nodes.length) out.push({ sourceKey: l.key, node: l.nodes[round] });
751
+ }
752
+ }
753
+ } else if (mergeOrder === "proportional") {
754
+ const tagged = lists.flatMap((l, sIdx) =>
755
+ l.nodes.map((node, i) => ({
756
+ sourceKey: l.key,
757
+ node,
758
+ sIdx,
759
+ i,
760
+ sortKey: (i + 0.5) / (l.weight > 0 ? l.weight : 1),
761
+ })),
762
+ );
763
+ tagged.sort((a, b) => a.sortKey - b.sortKey || a.sIdx - b.sIdx || a.i - b.i);
764
+ for (const t of tagged) out.push({ sourceKey: t.sourceKey, node: t.node });
765
+ } else {
766
+ for (const l of lists) {
767
+ for (const node of l.nodes) out.push({ sourceKey: l.key, node });
768
+ }
769
+ }
770
+ return out;
771
+ });
772
+
773
+ // -- Merged mode: one fixed-size-paged list over the cumulative results -----
774
+
775
+ const mergedGlobalPagination = computed<GlobalPagination>(() => {
776
+ // Options come from the global pagination config, NOT the in-scope source —
777
+ // so narrowing the dropdown to one object keeps the global page sizes.
778
+ const pageSizeOptions = pagination.pageSizeOptions;
779
+ const pageCount =
780
+ totalCount.value === 0 ? 0 : Math.ceil(totalCount.value / globalPageSize.value);
781
+ // Clamp in case totalCount shrank (e.g. a filter) below the current page.
782
+ const pageIndex = pageCount === 0 ? 0 : Math.min(globalPageIndex.value, pageCount - 1);
783
+ return {
784
+ pageIndex,
785
+ pageCount,
786
+ pageSize: globalPageSize.value,
787
+ pageSizeOptions,
788
+ hasNextPage: pageIndex + 1 < pageCount,
789
+ hasPreviousPage: pageIndex > 0,
790
+ totalCount: totalCount.value,
791
+ goToNextPage: () => {
792
+ globalPageIndex.value += 1;
793
+ },
794
+ goToPreviousPage: () => {
795
+ globalPageIndex.value = Math.max(0, globalPageIndex.value - 1);
796
+ },
797
+ goToPage: (next: number) => {
798
+ globalPageIndex.value = Math.max(0, Math.floor(next));
799
+ },
800
+ // Validate the requested size against the configured options before it
801
+ // becomes `$cmsLimit` / `first`, matching the per-source setter.
802
+ setPageSize: (size: number) => {
803
+ globalPageSize.value = validatePageSize(pagination, size);
804
+ globalPageIndex.value = 0;
805
+ },
806
+ };
807
+ });
808
+
809
+ const mergedModeResults = computed<MergedResultItem[]>(() => {
810
+ const start = mergedGlobalPagination.value.pageIndex * globalPageSize.value;
811
+ return concatenatedNodes.value.slice(start, start + globalPageSize.value);
812
+ });
813
+
814
+ // -- Per-source mode: independent cursors, "global page" = furthest source --
815
+ //
816
+ // Sources paginate with independent cursors (there is no global offset), so a
817
+ // "global page" is the furthest-advanced in-scope source's page. A source
818
+ // that runs out of pages is left behind at a lower pageIndex and stops
819
+ // contributing nodes — its last page was already shown on an earlier global
820
+ // page, so re-emitting it would duplicate rows in the merged list.
821
+
822
+ const perSourceGlobalPagination = computed<GlobalPagination>(() => {
823
+ const sources = inScopeSources.value;
824
+ const lead = sources[0];
825
+ const pageSize = lead?.pagination.pageSize ?? pagination.pageSize;
826
+ const pageSizeOptions = pagination.pageSizeOptions;
827
+
828
+ const pageIndex = sources.reduce((max, c) => Math.max(max, c.pagination.pageIndex), 0);
829
+
830
+ // Pages a source spans: prefer totalCount; otherwise a lower bound from its
831
+ // own position (current page, plus one more if it reports a next page).
832
+ const pageCount = sources.reduce((max, c) => {
833
+ const tc = c.result?.totalCount;
834
+ const span =
835
+ tc != null && pageSize > 0
836
+ ? Math.max(1, Math.ceil(tc / pageSize))
837
+ : c.pagination.pageIndex + (c.pagination.hasNextPage ? 2 : 1);
838
+ return Math.max(max, span);
839
+ }, 0);
840
+
841
+ const hasNextPage = sources.some((c) => c.pagination.hasNextPage);
842
+ const hasPreviousPage = pageIndex > 0;
843
+
844
+ const goToNextPage = () =>
845
+ sources.forEach((c) => {
846
+ if (c.pagination.hasNextPage) c.pagination.goToNextPage();
847
+ });
848
+ const goToPreviousPage = () =>
849
+ sources.forEach((c) => {
850
+ if (c.pagination.pageIndex === pageIndex) c.pagination.goToPreviousPage();
851
+ });
852
+
853
+ return {
854
+ pageIndex,
855
+ pageCount: totalCount.value === 0 ? 0 : pageCount,
856
+ pageSize,
857
+ pageSizeOptions,
858
+ hasNextPage,
859
+ hasPreviousPage,
860
+ totalCount: totalCount.value,
861
+ goToNextPage,
862
+ goToPreviousPage,
863
+ // Cursors can't seek to an arbitrary offset — honour adjacent pages only.
864
+ goToPage: (next: number) => {
865
+ if (next === pageIndex + 1 && hasNextPage) goToNextPage();
866
+ else if (next === pageIndex - 1 && hasPreviousPage) goToPreviousPage();
867
+ },
868
+ setPageSize: (size: number) => sources.forEach((c) => c.pagination.setPageSize(size)),
869
+ };
870
+ });
871
+
872
+ const perSourceResults = computed<MergedResultItem[]>(() => {
873
+ const idx = perSourceGlobalPagination.value.pageIndex;
874
+ return concatenatedNodes.value.filter((item) => {
875
+ const c = controllers.value[item.sourceKey];
876
+ // Only sources caught up to the current global page contribute; a
877
+ // left-behind source already showed its final page earlier.
878
+ return c?.pagination.pageIndex === idx;
879
+ });
880
+ });
881
+
882
+ const globalPagination = computed(() =>
883
+ mergedMode ? mergedGlobalPagination.value : perSourceGlobalPagination.value,
884
+ );
885
+ const mergedResults = computed(() =>
886
+ mergedMode ? mergedModeResults.value : perSourceResults.value,
887
+ );
888
+
889
+ // Reactive handle: refs/computeds are auto-unwrapped, so consumers (and
890
+ // templates) read plain `search.q`, `search.loading`, … like the React hook.
891
+ return reactive({
892
+ q,
893
+ setQ,
894
+ scope,
895
+ setScope,
896
+ scopeLocked,
897
+ sources: controllers,
898
+ inScopeSources,
899
+ mergedResults,
900
+ globalPagination,
901
+ loading,
902
+ error,
903
+ discoveredContentTypes,
904
+ resetAll,
905
+ }) as unknown as SearchHandle;
906
+ }