@salesforce/templates 66.13.4 → 66.13.6

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 (236) hide show
  1. package/lib/generators/baseGenerator.d.ts +2 -0
  2. package/lib/generators/baseGenerator.js +353 -0
  3. package/lib/generators/baseGenerator.js.map +1 -1
  4. package/lib/index.d.ts +1 -0
  5. package/lib/index.js +3 -1
  6. package/lib/index.js.map +1 -1
  7. package/lib/templates/project/angularextapp/CHANGELOG.md +258 -0
  8. package/lib/templates/project/angularextapp/_p_/_m_/_w_/_a_/angular.json +1 -1
  9. package/lib/templates/project/angularextapp/_p_/_m_/_w_/_a_/eslint-plugin-angular-graphql.js +148 -0
  10. package/lib/templates/project/angularextapp/_p_/_m_/_w_/_a_/eslint.config.js +9 -1
  11. package/lib/templates/project/angularextapp/_p_/_m_/_w_/_a_/package-lock.json +421 -394
  12. package/lib/templates/project/angularextapp/_p_/_m_/_w_/_a_/package.json +2 -2
  13. package/lib/templates/project/angularextapp/package.json +1 -1
  14. package/lib/templates/project/angularextapp/scripts/org-setup-config-schema.mjs +17 -0
  15. package/lib/templates/project/angularextapp/scripts/org-setup-utils.mjs +33 -3
  16. package/lib/templates/project/angularextapp/scripts/org-setup.mjs +535 -3
  17. package/lib/templates/project/angularintapp/CHANGELOG.md +266 -0
  18. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/angular.json +1 -1
  19. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/eslint-plugin-angular-graphql.js +148 -0
  20. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/eslint.config.js +9 -1
  21. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/package-lock.json +431 -404
  22. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/package.json +2 -2
  23. package/lib/templates/project/angularintapp/_p_/_m_/_w_/_a_/src/app/features/agentforce/agentforce-embed.service.ts +6 -1
  24. package/lib/templates/project/angularintapp/package.json +1 -1
  25. package/lib/templates/project/angularintapp/scripts/org-setup-config-schema.mjs +17 -0
  26. package/lib/templates/project/angularintapp/scripts/org-setup-utils.mjs +33 -3
  27. package/lib/templates/project/angularintapp/scripts/org-setup.mjs +535 -3
  28. package/lib/templates/project/reactexternalapp/CHANGELOG.md +1313 -0
  29. package/lib/templates/project/reactexternalapp/PRD.md +38 -0
  30. package/lib/templates/project/reactexternalapp/README.md +14 -10
  31. package/lib/templates/project/reactexternalapp/_p_/_m_/_ps_/reactexternalapp_Guest_User_Api_Access.permissionset-meta.xml +25 -0
  32. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +660 -692
  33. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +9 -7
  34. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/scripts/get-graphql-schema.mjs +8 -5
  35. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/components/SocialLogin.tsx +141 -0
  36. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Login.tsx +2 -0
  37. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/api/distinctValuesService.ts +84 -0
  38. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/api/searchService.ts +71 -0
  39. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/MergedSearchResults.tsx +277 -0
  40. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/Search.tsx +185 -0
  41. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/SearchResults.tsx +77 -0
  42. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/SourceSection.tsx +182 -0
  43. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/PaginationControls.tsx +185 -0
  44. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/ScopeSelector.tsx +55 -0
  45. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/SearchBar.tsx +55 -0
  46. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/SortControl.tsx +62 -0
  47. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/ActiveFilters.tsx +61 -0
  48. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/DefaultFilterPanel.tsx +125 -0
  49. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/{_os_/components → search/components/filters}/FilterContext.tsx +7 -20
  50. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  51. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
  52. package/lib/templates/project/{reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters → reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs}/FilterFieldWrapper.tsx +11 -6
  53. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/MultiSelectFilter.tsx +89 -0
  54. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/NumericRangeFilter.tsx +108 -0
  55. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/SelectFilter.tsx +46 -0
  56. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/TextFilter.tsx +52 -0
  57. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/results/DefaultResultRow.tsx +109 -0
  58. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/config.json +83 -0
  59. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/{_os_ → search}/hooks/useAsyncData.ts +3 -10
  60. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/hooks/useDistinctValues.ts +59 -0
  61. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/hooks/useSearch.ts +703 -0
  62. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/index.ts +90 -0
  63. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/loadConfig.ts +14 -0
  64. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/queryBuilder.ts +156 -0
  65. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/types.ts +307 -0
  66. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/debounce.ts +17 -0
  67. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/fieldUtils.ts +14 -0
  68. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/filterUtils.ts +285 -0
  69. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/sortUtils.ts +34 -0
  70. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +24 -8
  71. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/appLayout.tsx +7 -0
  72. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/CustomerWebClientChat.tsx +74 -0
  73. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/{_f_/_os_/components → components}/ObjectBreadcrumb.tsx +7 -2
  74. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/button.tsx +1 -1
  75. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/datePicker.tsx +2 -2
  76. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/table.tsx +9 -2
  77. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/index.ts +6 -0
  78. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +49 -16
  79. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +21 -293
  80. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/Home.tsx +22 -7
  81. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/TestChatPage.tsx +26 -0
  82. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +11 -5
  83. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/types/chat.ts +36 -0
  84. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/{_f_/_os_/utils/fieldUtils.ts → utils/accountFields.ts} +9 -0
  85. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/tsconfig.json +2 -2
  86. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/tsconfig.tsbuildinfo +1 -0
  87. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleSocialLoginConfig.cls +133 -0
  88. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleSocialLoginConfig.cls-meta.xml +5 -0
  89. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleSocialLoginConfig_Test.cls +101 -0
  90. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleSocialLoginConfig_Test.cls-meta.xml +5 -0
  91. package/lib/templates/project/reactexternalapp/_p_/_m_/package.xml +4 -0
  92. package/lib/templates/project/reactexternalapp/package.json +8 -2
  93. package/lib/templates/project/reactexternalapp/scripts/org-setup-config-schema.mjs +120 -0
  94. package/lib/templates/project/reactexternalapp/scripts/org-setup-dev.mjs +87 -0
  95. package/lib/templates/project/reactexternalapp/scripts/org-setup-utils.mjs +569 -0
  96. package/lib/templates/project/reactexternalapp/scripts/org-setup-xml.mjs +182 -0
  97. package/lib/templates/project/reactexternalapp/scripts/org-setup.config.json +6 -2
  98. package/lib/templates/project/reactexternalapp/scripts/org-setup.mjs +1509 -328
  99. package/lib/templates/project/reactexternalapp/scripts/validate-org-setup-config.mjs +38 -0
  100. package/lib/templates/project/reactinternalapp/CHANGELOG.md +1127 -0
  101. package/lib/templates/project/reactinternalapp/PRD.md +35 -0
  102. package/lib/templates/project/reactinternalapp/README.md +14 -10
  103. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +509 -505
  104. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +9 -8
  105. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/api/distinctValuesService.ts +84 -0
  106. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/api/searchService.ts +71 -0
  107. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/MergedSearchResults.tsx +277 -0
  108. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/Search.tsx +185 -0
  109. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/SearchResults.tsx +77 -0
  110. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/SourceSection.tsx +182 -0
  111. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/PaginationControls.tsx +185 -0
  112. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/ScopeSelector.tsx +55 -0
  113. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/SearchBar.tsx +55 -0
  114. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/controls/SortControl.tsx +62 -0
  115. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/ActiveFilters.tsx +61 -0
  116. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/DefaultFilterPanel.tsx +125 -0
  117. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/{_os_/components → search/components/filters}/FilterContext.tsx +7 -20
  118. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  119. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
  120. package/lib/templates/project/{reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters → reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs}/FilterFieldWrapper.tsx +11 -6
  121. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/MultiSelectFilter.tsx +89 -0
  122. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/NumericRangeFilter.tsx +108 -0
  123. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/SelectFilter.tsx +46 -0
  124. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/filters/inputs/TextFilter.tsx +52 -0
  125. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/components/results/DefaultResultRow.tsx +109 -0
  126. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/config.json +83 -0
  127. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/{_os_ → search}/hooks/useAsyncData.ts +3 -10
  128. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/hooks/useDistinctValues.ts +59 -0
  129. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/hooks/useSearch.ts +703 -0
  130. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/index.ts +90 -0
  131. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/loadConfig.ts +14 -0
  132. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/queryBuilder.ts +156 -0
  133. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/types.ts +307 -0
  134. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/debounce.ts +17 -0
  135. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/fieldUtils.ts +14 -0
  136. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/filterUtils.ts +285 -0
  137. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/search/utils/sortUtils.ts +34 -0
  138. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +16 -132
  139. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/{_f_/_os_/components → components}/ObjectBreadcrumb.tsx +7 -2
  140. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/button.tsx +1 -1
  141. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/datePicker.tsx +2 -2
  142. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/table.tsx +9 -2
  143. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +49 -16
  144. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +21 -293
  145. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/Home.tsx +21 -6
  146. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +4 -4
  147. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/{_f_/_os_/utils/fieldUtils.ts → utils/accountFields.ts} +9 -0
  148. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/tsconfig.json +2 -2
  149. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/tsconfig.tsbuildinfo +1 -0
  150. package/lib/templates/project/reactinternalapp/package.json +4 -2
  151. package/lib/templates/project/reactinternalapp/scripts/org-setup-config-schema.mjs +24 -0
  152. package/lib/templates/project/reactinternalapp/scripts/org-setup-dev.mjs +87 -0
  153. package/lib/templates/project/reactinternalapp/scripts/org-setup-utils.mjs +569 -0
  154. package/lib/templates/project/reactinternalapp/scripts/org-setup-xml.mjs +182 -0
  155. package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +1162 -214
  156. package/lib/templates/uiBundles/angularbasic/angular.json +1 -1
  157. package/lib/templates/uiBundles/angularbasic/eslint-plugin-angular-graphql.js +148 -0
  158. package/lib/templates/uiBundles/angularbasic/eslint.config.js +9 -1
  159. package/lib/templates/uiBundles/angularbasic/package-lock.json +421 -394
  160. package/lib/templates/uiBundles/angularbasic/package.json +2 -2
  161. package/lib/templates/uiBundles/reactbasic/package-lock.json +445 -418
  162. package/lib/utils/template-placeholders.js +7 -0
  163. package/lib/utils/template-placeholders.js.map +1 -1
  164. package/lib/utils/uiBundleTemplateUtils.d.ts +3 -1
  165. package/lib/utils/uiBundleTemplateUtils.js +4 -1
  166. package/lib/utils/uiBundleTemplateUtils.js.map +1 -1
  167. package/package.json +7 -7
  168. package/lib/templates/project/reactexternalapp/AGENT.md +0 -252
  169. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/accountSearchService.ts +0 -46
  170. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/distinctAccountIndustries.graphql +0 -19
  171. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/distinctAccountTypes.graphql +0 -19
  172. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/getAccountDetail.graphql +0 -121
  173. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/searchAccounts.graphql +0 -51
  174. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +0 -355
  175. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +0 -304
  176. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/Home.tsx +0 -34
  177. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +0 -82
  178. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/ActiveFilters.tsx +0 -89
  179. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/PaginationControls.tsx +0 -109
  180. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/SearchBar.tsx +0 -41
  181. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/SortControl.tsx +0 -143
  182. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/BooleanFilter.tsx +0 -78
  183. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateFilter.tsx +0 -128
  184. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateRangeFilter.tsx +0 -70
  185. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/MultiSelectFilter.tsx +0 -97
  186. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +0 -167
  187. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +0 -52
  188. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SelectFilter.tsx +0 -97
  189. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +0 -93
  190. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useDebouncedCallback.ts +0 -34
  191. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useObjectSearchParams.ts +0 -254
  192. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/debounce.ts +0 -25
  193. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/filterUtils.ts +0 -404
  194. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/sortUtils.ts +0 -38
  195. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/accountSearchService.ts +0 -46
  196. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountIndustries.graphql +0 -19
  197. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountTypes.graphql +0 -19
  198. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/searchAccounts.graphql +0 -51
  199. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/graphql-operations-types.ts +0 -11260
  200. package/lib/templates/project/reactexternalapp/scripts/prepare-import-unique-fields.js +0 -122
  201. package/lib/templates/project/reactinternalapp/AGENT.md +0 -252
  202. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/accountSearchService.ts +0 -46
  203. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/distinctAccountIndustries.graphql +0 -19
  204. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/distinctAccountTypes.graphql +0 -19
  205. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/getAccountDetail.graphql +0 -121
  206. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/api/query/searchAccounts.graphql +0 -51
  207. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +0 -355
  208. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +0 -304
  209. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/Home.tsx +0 -34
  210. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +0 -82
  211. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/ActiveFilters.tsx +0 -89
  212. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/PaginationControls.tsx +0 -109
  213. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/SearchBar.tsx +0 -41
  214. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/SortControl.tsx +0 -143
  215. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/BooleanFilter.tsx +0 -78
  216. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateFilter.tsx +0 -128
  217. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateRangeFilter.tsx +0 -70
  218. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/MultiSelectFilter.tsx +0 -97
  219. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +0 -167
  220. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +0 -52
  221. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SelectFilter.tsx +0 -97
  222. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +0 -93
  223. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useDebouncedCallback.ts +0 -34
  224. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useObjectSearchParams.ts +0 -254
  225. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/debounce.ts +0 -25
  226. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/filterUtils.ts +0 -404
  227. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/sortUtils.ts +0 -38
  228. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/accountSearchService.ts +0 -46
  229. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountIndustries.graphql +0 -19
  230. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountTypes.graphql +0 -19
  231. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/searchAccounts.graphql +0 -51
  232. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/graphql-operations-types.ts +0 -11260
  233. package/lib/templates/project/reactinternalapp/scripts/prepare-import-unique-fields.js +0 -122
  234. /package/lib/templates/project/angularextapp/_p_/_m_/{permissionsets → _ps_}/angularextapp_Guest_User_Api_Access.permissionset-meta.xml +0 -0
  235. /package/lib/templates/project/angularintapp/_p_/_m_/{permissionsets → _ps_}/angularintapp_Access.permissionset-meta.xml +0 -0
  236. /package/lib/templates/project/reactinternalapp/_p_/_m_/{permissionsets → _ps_}/reactinternalapp_Access.permissionset-meta.xml +0 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Drop-in search component. Given a `SearchConfig`, renders a complete
3
+ * search experience: search bar, an aggregate result count, and results.
4
+ *
5
+ * The results layout follows the config's pagination mode — no separate prop:
6
+ * - `pagination.mode: "per-source"` (default): one stacked section per
7
+ * source, each with its own filters, sort, and pagination.
8
+ * - `pagination.mode: "merged"`: a single combined grid across every in-scope
9
+ * source, driven by one global pager over the cumulative result set (with a
10
+ * results count and, under a single scope, that source's filter chrome).
11
+ *
12
+ * The simplest possible integration:
13
+ *
14
+ * ```tsx
15
+ * import { Search, config } from ".../features/search";
16
+ * export default function MySearchPage() {
17
+ * return <Search config={config} />;
18
+ * }
19
+ * ```
20
+ *
21
+ * Override slots:
22
+ * - `renderResult[sourceKey]` — replace the default row renderer for one
23
+ * source. Pass `false` to hide that source entirely.
24
+ * - `renderFilters[sourceKey]` — replace the default filter sidebar for
25
+ * one source. Pass `false` to suppress filters for that source.
26
+ * - `renderHeader` — replace the default header (title + subtitle).
27
+ * - `searchPlaceholder` — placeholder text for the search input.
28
+ * - `title`, `subtitle` — defaults passed to the built-in header.
29
+ */
30
+
31
+ import { useMemo, type ReactNode } from "react";
32
+ import { useSearch } from "../hooks/useSearch";
33
+ import { SearchBar } from "./controls/SearchBar";
34
+ import { ScopeSelector } from "./controls/ScopeSelector";
35
+ import { SearchResults } from "./SearchResults";
36
+ import { MergedSearchResults } from "./MergedSearchResults";
37
+ import { ALL_SCOPE } from "../types";
38
+ import type { SearchConfig, SearchHandle, SObjectSourceConfig } from "../types";
39
+
40
+ type ResultRenderer = ((node: unknown) => ReactNode) | false;
41
+ type FilterRenderer = (() => ReactNode) | false;
42
+
43
+ /**
44
+ * Locks the search to a single source. Reuses the same shared `config.json`
45
+ * but shows only results matching the given (kind, key) tuple.
46
+ *
47
+ * When set:
48
+ * - The scope dropdown is hidden.
49
+ * - URL `?scope=` is not written (the page route already implies the source).
50
+ * - Only the matching source is fetched and rendered.
51
+ */
52
+ export interface RestrictTo {
53
+ kind: SObjectSourceConfig["kind"];
54
+ key: string;
55
+ }
56
+
57
+ interface SearchProps {
58
+ config: SearchConfig;
59
+ renderResult?: Record<string, ResultRenderer>;
60
+ renderFilters?: Record<string, FilterRenderer>;
61
+ emptyMessages?: Record<string, string>;
62
+ searchPlaceholder?: string;
63
+ title?: string;
64
+ subtitle?: string;
65
+ /**
66
+ * Lock the search to a single source by `(kind, key)`. Hides the scope
67
+ * dropdown and forces results to that one source. Useful for embedding
68
+ * a single-object search inside a page where the source is implicit.
69
+ */
70
+ restrictTo?: RestrictTo;
71
+ /**
72
+ * Show the source-scope dropdown next to the search bar.
73
+ * Defaults to `true` when the config has 2+ sources, `false` otherwise
74
+ * (a one-source dropdown would be redundant). Always hidden when
75
+ * `restrictTo` is set.
76
+ */
77
+ showScopeSelector?: boolean;
78
+ /** Label for the "search everything" entry in the scope dropdown. */
79
+ allScopeLabel?: string;
80
+ /** Optional override for the entire header region. */
81
+ renderHeader?: (handle: SearchHandle) => ReactNode;
82
+ className?: string;
83
+ }
84
+
85
+ export function Search({
86
+ config,
87
+ renderResult,
88
+ renderFilters,
89
+ emptyMessages,
90
+ searchPlaceholder = "Search…",
91
+ title = "Search",
92
+ subtitle,
93
+ restrictTo,
94
+ showScopeSelector,
95
+ allScopeLabel,
96
+ renderHeader,
97
+ className,
98
+ }: SearchProps) {
99
+ // Resolve restrictTo to a lockedScope value, surfacing a clear error if
100
+ // the (kind, key) pair doesn't match any source. Memoize so it's
101
+ // stable across renders.
102
+ const lockedScope = useMemo<string | undefined>(() => {
103
+ if (!restrictTo) return undefined;
104
+ const match = config.sources.find(
105
+ (s) => s.kind === restrictTo.kind && s.key === restrictTo.key,
106
+ );
107
+ if (!match) {
108
+ console.warn(
109
+ `<Search restrictTo>: no source with kind="${restrictTo.kind}" and key="${restrictTo.key}" in config. Falling back to "all".`,
110
+ );
111
+ return undefined;
112
+ }
113
+ return match.key;
114
+ }, [restrictTo, config.sources]);
115
+
116
+ const handle = useSearch(config, { lockedScope });
117
+ // The results layout follows the pagination mode — a "merged" config means a
118
+ // single combined grid, "per-source" (default) means one section per source.
119
+ // There is no separate `layout` prop: the two would only ever be set in lockstep.
120
+ const merged = config.pagination?.mode === "merged";
121
+ const totalResults = Object.values(handle.sources).reduce(
122
+ (acc, controller) => acc + (controller.result?.totalCount ?? 0),
123
+ 0,
124
+ );
125
+ const shouldShowScope = !handle.scopeLocked && (showScopeSelector ?? config.sources.length >= 2);
126
+ // The aggregate count is only meaningful when results from more than one
127
+ // source are combined. With a single configured source, a locked scope, or
128
+ // a specific source picked in the dropdown, exactly one source is on screen
129
+ // and that source's own section already shows its count — so suppress the
130
+ // redundant total here.
131
+ const sourcesInScope = handle.scope === ALL_SCOPE ? config.sources.length : 1;
132
+ // In merged mode the results grid renders its own count row directly above the
133
+ // grid (in every scope), so the inline aggregate here would be redundant.
134
+ const showTotalResults = !merged && sourcesInScope >= 2;
135
+
136
+ return (
137
+ <div className={`max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6 ${className ?? ""}`}>
138
+ {renderHeader ? (
139
+ renderHeader(handle)
140
+ ) : (
141
+ <header className="space-y-2">
142
+ <h1 className="text-2xl font-bold">{title}</h1>
143
+ {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
144
+ </header>
145
+ )}
146
+
147
+ <div className="flex flex-wrap items-center gap-3">
148
+ <SearchBar value={handle.q} onChange={handle.setQ} placeholder={searchPlaceholder} />
149
+ {shouldShowScope && (
150
+ <ScopeSelector
151
+ config={config}
152
+ scope={handle.scope}
153
+ onScopeChange={handle.setScope}
154
+ allLabel={allScopeLabel}
155
+ />
156
+ )}
157
+ </div>
158
+
159
+ {merged ? (
160
+ <MergedSearchResults
161
+ handle={handle}
162
+ renderResult={renderResult}
163
+ emptyMessage={emptyMessages?.[handle.scope]}
164
+ />
165
+ ) : (
166
+ <>
167
+ {/* Aggregate count on its own row, directly above the results —
168
+ only in "All" scope, where the per-source sections don't each
169
+ carry a single meaningful total. */}
170
+ {showTotalResults && !handle.loading && (
171
+ <p className="text-sm text-muted-foreground" role="status">
172
+ {totalResults} total result{totalResults === 1 ? "" : "s"}
173
+ </p>
174
+ )}
175
+ <SearchResults
176
+ handle={handle}
177
+ renderResult={renderResult}
178
+ renderFilters={renderFilters}
179
+ emptyMessages={emptyMessages}
180
+ />
181
+ </>
182
+ )}
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Renders one SourceSection per configured source, in declaration order.
3
+ *
4
+ * Customization is optional — sensible defaults kick in for any source you
5
+ * don't override:
6
+ * - `renderResult[sourceKey]` — per-node renderer. Default: title from
7
+ * `displayFields[0]`, subtitle from the rest, optional `<Link>` driven
8
+ * by `routePattern`.
9
+ * - `renderFilters[sourceKey]` — sidebar filter UI. Default: one input per
10
+ * `filterBy` entry, with picklist options resolved from inline
11
+ * config or auto-fetched from the GraphQL aggregate API.
12
+ *
13
+ * Pass `false` for either entry to suppress the default for that source.
14
+ *
15
+ * Filter chrome (sidebar + active-filter chips + sort dropdown) is
16
+ * automatically hidden when `handle.scope === "all"` — per-source controls
17
+ * would be unreachable with every source on screen, and dangling chips
18
+ * would be confusing. Pre-existing filter / sort selections stay in state,
19
+ * so narrowing the scope brings them back unchanged.
20
+ */
21
+
22
+ import type { ReactNode } from "react";
23
+ import { SourceSection } from "./SourceSection";
24
+ import { DefaultResultRow } from "./results/DefaultResultRow";
25
+ import { DefaultFilterPanel } from "./filters/DefaultFilterPanel";
26
+ import { ALL_SCOPE, type SearchHandle } from "../types";
27
+
28
+ type ResultRenderer = ((node: unknown) => ReactNode) | false;
29
+ type FilterRenderer = (() => ReactNode) | false;
30
+
31
+ interface SearchResultsProps {
32
+ handle: SearchHandle;
33
+ renderResult?: Record<string, ResultRenderer>;
34
+ renderFilters?: Record<string, FilterRenderer>;
35
+ emptyMessages?: Record<string, string>;
36
+ }
37
+
38
+ export function SearchResults({
39
+ handle,
40
+ renderResult,
41
+ renderFilters,
42
+ emptyMessages,
43
+ }: SearchResultsProps) {
44
+ const filtersHidden = handle.scope === ALL_SCOPE;
45
+ return (
46
+ <div className="space-y-10">
47
+ {Object.entries(handle.sources).map(([key, controller]) => {
48
+ if (handle.scope !== ALL_SCOPE && handle.scope !== key) return null;
49
+ const overrideResult = renderResult?.[key];
50
+ if (overrideResult === false) return null;
51
+ const resolvedRenderResult: (node: unknown) => ReactNode =
52
+ overrideResult ?? ((node) => <DefaultResultRow node={node} source={controller.config} />);
53
+
54
+ const overrideFilters = renderFilters?.[key];
55
+ const hasFilterFields = (controller.config.filterBy?.length ?? 0) > 0;
56
+ const resolvedRenderFilters: (() => ReactNode) | undefined =
57
+ overrideFilters === false
58
+ ? undefined
59
+ : (overrideFilters ??
60
+ (hasFilterFields
61
+ ? () => <DefaultFilterPanel source={controller.config} />
62
+ : undefined));
63
+
64
+ return (
65
+ <SourceSection
66
+ key={key}
67
+ controller={controller}
68
+ renderResult={resolvedRenderResult}
69
+ renderFilters={resolvedRenderFilters}
70
+ hideFilterChrome={filtersHidden}
71
+ emptyMessage={emptyMessages?.[key]}
72
+ />
73
+ );
74
+ })}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Renders one source's slice of a search result: optional filter
3
+ * panel, sort dropdown, active-filter chips, the result list (delegated to
4
+ * the caller via `renderResult`), and pagination controls.
5
+ *
6
+ * The component is "smart" only about orchestration. The actual list-item UI
7
+ * is owned by the application via the `renderResult(node)` callback —
8
+ * search never decides how an Account or Contact card looks.
9
+ */
10
+
11
+ import type { ReactNode } from "react";
12
+ import { AlertCircle, SearchX } from "lucide-react";
13
+ import { Alert, AlertDescription, AlertTitle } from "../../../components/ui/alert";
14
+ import { Skeleton } from "../../../components/ui/skeleton";
15
+ import { ActiveFilters } from "./filters/ActiveFilters";
16
+ import { FilterProvider, FilterResetButton } from "./filters/FilterContext";
17
+ import { PaginationControls } from "./controls/PaginationControls";
18
+ import { SortControl } from "./controls/SortControl";
19
+ import type { SourceController } from "../types";
20
+
21
+ interface SourceSectionProps {
22
+ controller: SourceController;
23
+ renderResult: (node: unknown) => ReactNode;
24
+ /** Optional filter UI (TextFilter, SelectFilter, ...) rendered above the results. */
25
+ renderFilters?: () => ReactNode;
26
+ /**
27
+ * Suppress the filter panel, the active-filter chips, and the sort
28
+ * dropdown. Pre-existing filter / sort selections stay in state (and
29
+ * apply to the GraphQL query) so the caller can restore the chrome
30
+ * without losing the user's selections, but no chrome surface is rendered.
31
+ *
32
+ * Set by `<SearchResults>` when the current scope is "all" — per-source
33
+ * controls would be unreachable while every source is on screen, and
34
+ * dangling chips would be confusing.
35
+ */
36
+ hideFilterChrome?: boolean;
37
+ emptyMessage?: string;
38
+ className?: string;
39
+ }
40
+
41
+ export function SourceSection({
42
+ controller,
43
+ renderResult,
44
+ renderFilters,
45
+ hideFilterChrome,
46
+ emptyMessage,
47
+ className,
48
+ }: SourceSectionProps) {
49
+ const { config, result, loading, error, filters, sort, pagination } = controller;
50
+ const nodes = result?.nodes ?? [];
51
+ const totalCount = result?.totalCount;
52
+ const showResults = !loading && !error && nodes.length > 0;
53
+ const showEmpty = !loading && !error && nodes.length === 0;
54
+
55
+ const pageSizeOptions = pagination.pageSizeOptions ?? [pagination.pageSize];
56
+
57
+ const showFilterPanel = !!renderFilters && !hideFilterChrome;
58
+ const showActiveChips = !hideFilterChrome && filters.active.length > 0;
59
+ const showSortControl = !hideFilterChrome && (config.sortBy?.length ?? 0) > 0;
60
+
61
+ // Persistent, always-mounted live region: screen readers announce result
62
+ // updates (searching → count → empty → error) without an Enter keypress.
63
+ const liveMessage = loading
64
+ ? "Searching…"
65
+ : error
66
+ ? `Failed to load ${config.label}.`
67
+ : showEmpty
68
+ ? (emptyMessage ?? `No ${config.label.toLowerCase()} found.`)
69
+ : totalCount != null
70
+ ? `${config.label}: ${totalCount} ${totalCount === 1 ? "result" : "results"}`
71
+ : "";
72
+
73
+ return (
74
+ <section className={`flex flex-col gap-6 ${className ?? ""}`}>
75
+ <p className="sr-only" role="status" aria-live="polite">
76
+ {liveMessage}
77
+ </p>
78
+
79
+ {showFilterPanel && (
80
+ <FilterProvider
81
+ filters={filters.active}
82
+ onFilterChange={filters.set}
83
+ onFilterRemove={filters.remove}
84
+ onReset={() => filters.active.forEach((f) => filters.remove(f.field))}
85
+ >
86
+ <div className="rounded-md border p-3 space-y-3">
87
+ <div className="flex items-center justify-between">
88
+ <h2 className="text-sm font-semibold">Filters</h2>
89
+ <FilterResetButton size="sm" />
90
+ </div>
91
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
92
+ {renderFilters!()}
93
+ </div>
94
+ </div>
95
+ </FilterProvider>
96
+ )}
97
+
98
+ <div className="min-w-0">
99
+ <header className="flex flex-wrap items-baseline gap-3 mb-3">
100
+ <h2 className="text-lg font-semibold">{config.label}</h2>
101
+ </header>
102
+
103
+ {(showSortControl || showActiveChips) && (
104
+ <div className="flex flex-wrap items-center gap-2 mb-3">
105
+ {showSortControl && config.sortBy ? (
106
+ <SortControl configs={config.sortBy} sort={sort.current} onSortChange={sort.set} />
107
+ ) : null}
108
+ {showActiveChips && (
109
+ <ActiveFilters filters={filters.active} onRemove={filters.remove} />
110
+ )}
111
+ </div>
112
+ )}
113
+
114
+ {/* Result count, shown just above the results in every scope —
115
+ including a locked / single-source (restrictTo) view. */}
116
+ {!loading && !error && totalCount != null && (
117
+ <p className="text-sm text-muted-foreground mb-3">
118
+ {totalCount} {totalCount === 1 ? "result" : "results"}
119
+ </p>
120
+ )}
121
+
122
+ <div className="min-h-32">
123
+ {loading && (
124
+ <div className="space-y-2">
125
+ {Array.from({ length: Math.min(pagination.pageSize, 5) }, (_, i) => (
126
+ <Skeleton key={i} className="h-12 w-full" />
127
+ ))}
128
+ </div>
129
+ )}
130
+
131
+ {error && (
132
+ <Alert variant="destructive" role="alert">
133
+ <AlertCircle />
134
+ <AlertTitle>Failed to load {config.label}</AlertTitle>
135
+ <AlertDescription>{error}</AlertDescription>
136
+ </Alert>
137
+ )}
138
+
139
+ {showResults && (
140
+ <ul className="divide-y">
141
+ {nodes.map((node, i) => (
142
+ <li key={getNodeKey(node, config.idField, i)} className="py-3">
143
+ {renderResult(node)}
144
+ </li>
145
+ ))}
146
+ </ul>
147
+ )}
148
+
149
+ {showEmpty && (
150
+ <div className="flex flex-col items-center justify-center py-10 text-center">
151
+ <SearchX className="size-10 text-muted-foreground mb-3" />
152
+ <p className="text-sm text-muted-foreground">
153
+ {emptyMessage ?? `No ${config.label.toLowerCase()} found.`}
154
+ </p>
155
+ </div>
156
+ )}
157
+ </div>
158
+
159
+ <PaginationControls
160
+ pageIndex={pagination.pageIndex}
161
+ hasNextPage={pagination.hasNextPage}
162
+ hasPreviousPage={pagination.hasPreviousPage}
163
+ pageSize={pagination.pageSize}
164
+ pageSizeOptions={pageSizeOptions}
165
+ onNextPage={pagination.goToNextPage}
166
+ onPreviousPage={pagination.goToPreviousPage}
167
+ onPageSizeChange={pagination.setPageSize}
168
+ disabled={loading || !!error}
169
+ idPrefix={`page-size-${config.key}`}
170
+ />
171
+ </div>
172
+ </section>
173
+ );
174
+ }
175
+
176
+ function getNodeKey(node: unknown, idField: string | undefined, fallback: number): string | number {
177
+ if (node && typeof node === "object") {
178
+ const id = (node as Record<string, unknown>)[idField ?? "Id"];
179
+ if (typeof id === "string" || typeof id === "number") return id;
180
+ }
181
+ return fallback;
182
+ }
@@ -0,0 +1,185 @@
1
+ import {
2
+ Pagination,
3
+ PaginationContent,
4
+ PaginationEllipsis,
5
+ PaginationItem,
6
+ PaginationLink,
7
+ PaginationNext,
8
+ PaginationPrevious,
9
+ } from "../../../../components/ui/pagination";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "../../../../components/ui/select";
17
+ import { Label } from "../../../../components/ui/label";
18
+ import type { KeyboardEvent } from "react";
19
+
20
+ interface PaginationControlsProps {
21
+ pageIndex: number;
22
+ hasNextPage: boolean;
23
+ hasPreviousPage: boolean;
24
+ pageSize: number;
25
+ pageSizeOptions: readonly number[];
26
+ onNextPage: () => void;
27
+ onPreviousPage: () => void;
28
+ onPageSizeChange: (size: number) => void;
29
+ /**
30
+ * Total number of pages. When provided (with {@link onGoToPage}) and `> 1`,
31
+ * the control renders numbered page buttons with ellipsis truncation instead
32
+ * of a plain "Page N" label. Omit for cursor-style sources where the total
33
+ * page count is unknown.
34
+ */
35
+ pageCount?: number;
36
+ /** Jump to a 0-based page. Required to render numbered page buttons. */
37
+ onGoToPage?: (pageIndex: number) => void;
38
+ disabled?: boolean;
39
+ idPrefix?: string;
40
+ }
41
+
42
+ /**
43
+ * Builds the list of page tokens to render: 0-based page numbers plus
44
+ * `"ellipsis"` gap markers. Always shows the first and last page, a window
45
+ * around the current page, and collapses the rest. e.g. for current=5,
46
+ * total=12 → [0, ellipsis, 4, 5, 6, ellipsis, 11].
47
+ */
48
+ function getPageTokens(currentIndex: number, pageCount: number): Array<number | "ellipsis"> {
49
+ const SIBLINGS = 1; // pages shown on each side of the current page
50
+ const last = pageCount - 1;
51
+ const pages = new Set<number>([0, last]);
52
+ for (let p = currentIndex - SIBLINGS; p <= currentIndex + SIBLINGS; p++) {
53
+ if (p >= 0 && p <= last) pages.add(p);
54
+ }
55
+ const sorted = [...pages].sort((a, b) => a - b);
56
+ const tokens: Array<number | "ellipsis"> = [];
57
+ let prev = -1;
58
+ for (const p of sorted) {
59
+ if (prev !== -1 && p - prev > 1) tokens.push("ellipsis");
60
+ tokens.push(p);
61
+ prev = p;
62
+ }
63
+ return tokens;
64
+ }
65
+
66
+ export function PaginationControls({
67
+ pageIndex,
68
+ hasNextPage,
69
+ hasPreviousPage,
70
+ pageSize,
71
+ pageSizeOptions,
72
+ onNextPage,
73
+ onPreviousPage,
74
+ onPageSizeChange,
75
+ pageCount,
76
+ onGoToPage,
77
+ disabled = false,
78
+ idPrefix = "page-size",
79
+ }: PaginationControlsProps) {
80
+ const handlePageSizeChange = (next: string) => {
81
+ const size = parseInt(next, 10);
82
+ if (!isNaN(size) && size !== pageSize) onPageSizeChange(size);
83
+ };
84
+ const prevDisabled = disabled || !hasPreviousPage;
85
+ const nextDisabled = disabled || !hasNextPage;
86
+ const selectId = `${idPrefix}-select`;
87
+
88
+ // Numbered page buttons require both a known page count and a seek callback.
89
+ const showNumberedPages = pageCount != null && pageCount > 1 && onGoToPage != null;
90
+
91
+ // The underlying pagination control renders an <a> without an href, so it is
92
+ // not keyboard-operable on its own. Activate it on Enter/Space to match the
93
+ // behaviour of a native button (WCAG 2.2 SC 2.1.1 Keyboard).
94
+ const handleActivationKey = (action: () => void) => (event: KeyboardEvent) => {
95
+ if (event.key === "Enter" || event.key === " ") {
96
+ event.preventDefault();
97
+ action();
98
+ }
99
+ };
100
+
101
+ return (
102
+ <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center gap-4 py-2">
103
+ <div className="flex justify-center sm:justify-start items-center gap-2">
104
+ <Label htmlFor={selectId} className="text-sm font-normal whitespace-nowrap">
105
+ Results per page:
106
+ </Label>
107
+ <Select
108
+ value={pageSize.toString()}
109
+ onValueChange={handlePageSizeChange}
110
+ disabled={disabled}
111
+ >
112
+ <SelectTrigger id={selectId} className="w-16">
113
+ <SelectValue />
114
+ </SelectTrigger>
115
+ <SelectContent>
116
+ {pageSizeOptions.map((size) => (
117
+ <SelectItem key={size} value={size.toString()}>
118
+ {size}
119
+ </SelectItem>
120
+ ))}
121
+ </SelectContent>
122
+ </Select>
123
+ </div>
124
+ <Pagination className="w-full mx-0 sm:justify-end">
125
+ <PaginationContent>
126
+ <PaginationItem>
127
+ <PaginationPrevious
128
+ role="button"
129
+ tabIndex={prevDisabled ? -1 : 0}
130
+ onClick={prevDisabled ? undefined : onPreviousPage}
131
+ onKeyDown={prevDisabled ? undefined : handleActivationKey(onPreviousPage)}
132
+ aria-disabled={prevDisabled}
133
+ className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
134
+ />
135
+ </PaginationItem>
136
+
137
+ {showNumberedPages ? (
138
+ getPageTokens(pageIndex, pageCount).map((token, i) =>
139
+ token === "ellipsis" ? (
140
+ <PaginationItem key={`ellipsis-${i}`}>
141
+ <PaginationEllipsis />
142
+ </PaginationItem>
143
+ ) : (
144
+ <PaginationItem key={token}>
145
+ <PaginationLink
146
+ role="button"
147
+ tabIndex={disabled ? -1 : 0}
148
+ isActive={token === pageIndex}
149
+ aria-label={`Go to page ${token + 1}`}
150
+ aria-current={token === pageIndex ? "page" : undefined}
151
+ onClick={disabled ? undefined : () => onGoToPage(token)}
152
+ onKeyDown={disabled ? undefined : handleActivationKey(() => onGoToPage(token))}
153
+ aria-disabled={disabled}
154
+ className={disabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
155
+ >
156
+ {token + 1}
157
+ </PaginationLink>
158
+ </PaginationItem>
159
+ ),
160
+ )
161
+ ) : (
162
+ <PaginationItem>
163
+ <span className="min-w-16 text-center text-sm text-muted-foreground px-2">
164
+ {pageCount != null && pageCount > 0
165
+ ? `Page ${pageIndex + 1} of ${pageCount}`
166
+ : `Page ${pageIndex + 1}`}
167
+ </span>
168
+ </PaginationItem>
169
+ )}
170
+
171
+ <PaginationItem>
172
+ <PaginationNext
173
+ role="button"
174
+ tabIndex={nextDisabled ? -1 : 0}
175
+ onClick={nextDisabled ? undefined : onNextPage}
176
+ onKeyDown={nextDisabled ? undefined : handleActivationKey(onNextPage)}
177
+ aria-disabled={nextDisabled}
178
+ className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
179
+ />
180
+ </PaginationItem>
181
+ </PaginationContent>
182
+ </Pagination>
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Dropdown that narrows the search to a single source (or "All").
3
+ *
4
+ * Sits next to the search bar in the default {@link Search} layout. The
5
+ * options are derived from the configured sources: an "All" entry plus one
6
+ * entry per source (using each source's `label`).
7
+ *
8
+ * Selecting a non-"All" scope tells {@link useSearch} to fetch and render
9
+ * only that source. Switching scope resets per-source pagination cursors —
10
+ * the visible result set is changing.
11
+ */
12
+
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from "../../../../components/ui/select";
20
+ import { ALL_SCOPE, type SearchConfig, type SearchScope } from "../../types";
21
+
22
+ interface ScopeSelectorProps {
23
+ config: SearchConfig;
24
+ scope: SearchScope;
25
+ onScopeChange: (scope: SearchScope) => void;
26
+ allLabel?: string;
27
+ className?: string;
28
+ }
29
+
30
+ export function ScopeSelector({
31
+ config,
32
+ scope,
33
+ onScopeChange,
34
+ allLabel = "All",
35
+ className,
36
+ }: ScopeSelectorProps) {
37
+ if (config.sources.length === 0) return null;
38
+ return (
39
+ <div className={className}>
40
+ <Select value={scope} onValueChange={onScopeChange}>
41
+ <SelectTrigger size="sm" className="min-w-[140px]" aria-label="Search scope">
42
+ <SelectValue />
43
+ </SelectTrigger>
44
+ <SelectContent>
45
+ <SelectItem value={ALL_SCOPE}>{allLabel}</SelectItem>
46
+ {config.sources.map((source) => (
47
+ <SelectItem key={source.key} value={source.key}>
48
+ {source.label}
49
+ </SelectItem>
50
+ ))}
51
+ </SelectContent>
52
+ </Select>
53
+ </div>
54
+ );
55
+ }