@endora-commerce/admin-shell 0.0.0-stage → 0.100.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 (428) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -2
  3. package/dist/AdminRoot.d.ts +45 -0
  4. package/dist/AdminRoot.d.ts.map +1 -0
  5. package/dist/AdminRoot.js +39 -0
  6. package/dist/AdminRoot.js.map +1 -0
  7. package/dist/App.d.ts +42 -0
  8. package/dist/App.d.ts.map +1 -0
  9. package/dist/App.js +94 -0
  10. package/dist/App.js.map +1 -0
  11. package/dist/components/AppShell.d.ts +5 -0
  12. package/dist/components/AppShell.d.ts.map +1 -0
  13. package/dist/components/AppShell.js +1259 -0
  14. package/dist/components/AppShell.js.map +1 -0
  15. package/dist/components/BrandLogo.d.ts +21 -0
  16. package/dist/components/BrandLogo.d.ts.map +1 -0
  17. package/dist/components/BrandLogo.js +21 -0
  18. package/dist/components/BrandLogo.js.map +1 -0
  19. package/dist/components/FederatedSignIn.d.ts +42 -0
  20. package/dist/components/FederatedSignIn.d.ts.map +1 -0
  21. package/dist/components/FederatedSignIn.js +56 -0
  22. package/dist/components/FederatedSignIn.js.map +1 -0
  23. package/dist/components/FileDropzone.d.ts +17 -0
  24. package/dist/components/FileDropzone.d.ts.map +1 -0
  25. package/dist/components/FileDropzone.js +17 -0
  26. package/dist/components/FileDropzone.js.map +1 -0
  27. package/dist/components/IdleLogout.d.ts +2 -0
  28. package/dist/components/IdleLogout.d.ts.map +1 -0
  29. package/dist/components/IdleLogout.js +90 -0
  30. package/dist/components/IdleLogout.js.map +1 -0
  31. package/dist/components/ImpersonationBanner.d.ts +13 -0
  32. package/dist/components/ImpersonationBanner.d.ts.map +1 -0
  33. package/dist/components/ImpersonationBanner.js +10 -0
  34. package/dist/components/ImpersonationBanner.js.map +1 -0
  35. package/dist/components/LanguagePicker.d.ts +3 -0
  36. package/dist/components/LanguagePicker.d.ts.map +1 -0
  37. package/dist/components/LanguagePicker.js +127 -0
  38. package/dist/components/LanguagePicker.js.map +1 -0
  39. package/dist/components/LoginPage.d.ts +11 -0
  40. package/dist/components/LoginPage.d.ts.map +1 -0
  41. package/dist/components/LoginPage.js +44 -0
  42. package/dist/components/LoginPage.js.map +1 -0
  43. package/dist/components/PaginationFooter.d.ts +18 -0
  44. package/dist/components/PaginationFooter.d.ts.map +1 -0
  45. package/dist/components/PaginationFooter.js +17 -0
  46. package/dist/components/PaginationFooter.js.map +1 -0
  47. package/dist/components/ResponsiveTable.d.ts +18 -0
  48. package/dist/components/ResponsiveTable.d.ts.map +1 -0
  49. package/dist/components/ResponsiveTable.js +17 -0
  50. package/dist/components/ResponsiveTable.js.map +1 -0
  51. package/dist/components/RowActionMenu.d.ts +18 -0
  52. package/dist/components/RowActionMenu.d.ts.map +1 -0
  53. package/dist/components/RowActionMenu.js +17 -0
  54. package/dist/components/RowActionMenu.js.map +1 -0
  55. package/dist/components/SpeechToTextButton.d.ts +14 -0
  56. package/dist/components/SpeechToTextButton.d.ts.map +1 -0
  57. package/dist/components/SpeechToTextButton.js +19 -0
  58. package/dist/components/SpeechToTextButton.js.map +1 -0
  59. package/dist/components/StickyFormActions.d.ts +18 -0
  60. package/dist/components/StickyFormActions.d.ts.map +1 -0
  61. package/dist/components/StickyFormActions.js +17 -0
  62. package/dist/components/StickyFormActions.js.map +1 -0
  63. package/dist/components/TouchReorderButtons.d.ts +18 -0
  64. package/dist/components/TouchReorderButtons.d.ts.map +1 -0
  65. package/dist/components/TouchReorderButtons.js +17 -0
  66. package/dist/components/TouchReorderButtons.js.map +1 -0
  67. package/dist/components/admin-user-picker/AdminUserPicker.d.ts +18 -0
  68. package/dist/components/admin-user-picker/AdminUserPicker.d.ts.map +1 -0
  69. package/dist/components/admin-user-picker/AdminUserPicker.js +17 -0
  70. package/dist/components/admin-user-picker/AdminUserPicker.js.map +1 -0
  71. package/dist/components/asset-picker/AssetFieldPicker.d.ts +21 -0
  72. package/dist/components/asset-picker/AssetFieldPicker.d.ts.map +1 -0
  73. package/dist/components/asset-picker/AssetFieldPicker.js +20 -0
  74. package/dist/components/asset-picker/AssetFieldPicker.js.map +1 -0
  75. package/dist/components/category-picker/CategorySelect.d.ts +18 -0
  76. package/dist/components/category-picker/CategorySelect.d.ts.map +1 -0
  77. package/dist/components/category-picker/CategorySelect.js +17 -0
  78. package/dist/components/category-picker/CategorySelect.js.map +1 -0
  79. package/dist/components/category-tree-picker/CategoryTreePicker.d.ts +18 -0
  80. package/dist/components/category-tree-picker/CategoryTreePicker.d.ts.map +1 -0
  81. package/dist/components/category-tree-picker/CategoryTreePicker.js +17 -0
  82. package/dist/components/category-tree-picker/CategoryTreePicker.js.map +1 -0
  83. package/dist/components/category-tree-picker/category-tree-utils.d.ts +18 -0
  84. package/dist/components/category-tree-picker/category-tree-utils.d.ts.map +1 -0
  85. package/dist/components/category-tree-picker/category-tree-utils.js +17 -0
  86. package/dist/components/category-tree-picker/category-tree-utils.js.map +1 -0
  87. package/dist/components/category-tree-picker/index.d.ts +18 -0
  88. package/dist/components/category-tree-picker/index.d.ts.map +1 -0
  89. package/dist/components/category-tree-picker/index.js +17 -0
  90. package/dist/components/category-tree-picker/index.js.map +1 -0
  91. package/dist/components/charts/echart.d.ts +18 -0
  92. package/dist/components/charts/echart.d.ts.map +1 -0
  93. package/dist/components/charts/echart.js +17 -0
  94. package/dist/components/charts/echart.js.map +1 -0
  95. package/dist/components/cms-picker/CmsBlockPicker.d.ts +19 -0
  96. package/dist/components/cms-picker/CmsBlockPicker.d.ts.map +1 -0
  97. package/dist/components/cms-picker/CmsBlockPicker.js +18 -0
  98. package/dist/components/cms-picker/CmsBlockPicker.js.map +1 -0
  99. package/dist/components/cms-picker/CmsPagePicker.d.ts +19 -0
  100. package/dist/components/cms-picker/CmsPagePicker.d.ts.map +1 -0
  101. package/dist/components/cms-picker/CmsPagePicker.js +18 -0
  102. package/dist/components/cms-picker/CmsPagePicker.js.map +1 -0
  103. package/dist/components/country-select/index.d.ts +18 -0
  104. package/dist/components/country-select/index.d.ts.map +1 -0
  105. package/dist/components/country-select/index.js +17 -0
  106. package/dist/components/country-select/index.js.map +1 -0
  107. package/dist/components/customer-group-picker/CustomerGroupPicker.d.ts +18 -0
  108. package/dist/components/customer-group-picker/CustomerGroupPicker.d.ts.map +1 -0
  109. package/dist/components/customer-group-picker/CustomerGroupPicker.js +17 -0
  110. package/dist/components/customer-group-picker/CustomerGroupPicker.js.map +1 -0
  111. package/dist/components/customer-picker/CustomerPicker.d.ts +18 -0
  112. package/dist/components/customer-picker/CustomerPicker.d.ts.map +1 -0
  113. package/dist/components/customer-picker/CustomerPicker.js +17 -0
  114. package/dist/components/customer-picker/CustomerPicker.js.map +1 -0
  115. package/dist/components/hooks/useFocusTrap.d.ts +17 -0
  116. package/dist/components/hooks/useFocusTrap.d.ts.map +1 -0
  117. package/dist/components/hooks/useFocusTrap.js +17 -0
  118. package/dist/components/hooks/useFocusTrap.js.map +1 -0
  119. package/dist/components/hooks/useSpeechToText.d.ts +15 -0
  120. package/dist/components/hooks/useSpeechToText.d.ts.map +1 -0
  121. package/dist/components/hooks/useSpeechToText.js +63 -0
  122. package/dist/components/hooks/useSpeechToText.js.map +1 -0
  123. package/dist/components/hooks/useViewportTier.d.ts +18 -0
  124. package/dist/components/hooks/useViewportTier.d.ts.map +1 -0
  125. package/dist/components/hooks/useViewportTier.js +17 -0
  126. package/dist/components/hooks/useViewportTier.js.map +1 -0
  127. package/dist/components/notifications/NotificationBell.d.ts +13 -0
  128. package/dist/components/notifications/NotificationBell.d.ts.map +1 -0
  129. package/dist/components/notifications/NotificationBell.js +154 -0
  130. package/dist/components/notifications/NotificationBell.js.map +1 -0
  131. package/dist/components/notifications/index.d.ts +3 -0
  132. package/dist/components/notifications/index.d.ts.map +1 -0
  133. package/dist/components/notifications/index.js +3 -0
  134. package/dist/components/notifications/index.js.map +1 -0
  135. package/dist/components/notifications/useAdminNotifications.d.ts +41 -0
  136. package/dist/components/notifications/useAdminNotifications.d.ts.map +1 -0
  137. package/dist/components/notifications/useAdminNotifications.js +86 -0
  138. package/dist/components/notifications/useAdminNotifications.js.map +1 -0
  139. package/dist/components/organization-picker/OrganizationPicker.d.ts +19 -0
  140. package/dist/components/organization-picker/OrganizationPicker.d.ts.map +1 -0
  141. package/dist/components/organization-picker/OrganizationPicker.js +18 -0
  142. package/dist/components/organization-picker/OrganizationPicker.js.map +1 -0
  143. package/dist/components/organization-picker/OrganizationPickerMulti.d.ts +19 -0
  144. package/dist/components/organization-picker/OrganizationPickerMulti.d.ts.map +1 -0
  145. package/dist/components/organization-picker/OrganizationPickerMulti.js +18 -0
  146. package/dist/components/organization-picker/OrganizationPickerMulti.js.map +1 -0
  147. package/dist/components/organization-picker/OrganizationStatusBadge.d.ts +19 -0
  148. package/dist/components/organization-picker/OrganizationStatusBadge.d.ts.map +1 -0
  149. package/dist/components/organization-picker/OrganizationStatusBadge.js +18 -0
  150. package/dist/components/organization-picker/OrganizationStatusBadge.js.map +1 -0
  151. package/dist/components/organization-picker/index.d.ts +19 -0
  152. package/dist/components/organization-picker/index.d.ts.map +1 -0
  153. package/dist/components/organization-picker/index.js +18 -0
  154. package/dist/components/organization-picker/index.js.map +1 -0
  155. package/dist/components/organization-picker/useOrganizationsQuery.d.ts +19 -0
  156. package/dist/components/organization-picker/useOrganizationsQuery.d.ts.map +1 -0
  157. package/dist/components/organization-picker/useOrganizationsQuery.js +18 -0
  158. package/dist/components/organization-picker/useOrganizationsQuery.js.map +1 -0
  159. package/dist/components/payment-method-availability/MethodAvailabilityCell.d.ts +17 -0
  160. package/dist/components/payment-method-availability/MethodAvailabilityCell.d.ts.map +1 -0
  161. package/dist/components/payment-method-availability/MethodAvailabilityCell.js +17 -0
  162. package/dist/components/payment-method-availability/MethodAvailabilityCell.js.map +1 -0
  163. package/dist/components/prompt-actions/PromptModePanel.d.ts +24 -0
  164. package/dist/components/prompt-actions/PromptModePanel.d.ts.map +1 -0
  165. package/dist/components/prompt-actions/PromptModePanel.js +115 -0
  166. package/dist/components/prompt-actions/PromptModePanel.js.map +1 -0
  167. package/dist/components/prompt-actions/PromptPlanPreview.d.ts +20 -0
  168. package/dist/components/prompt-actions/PromptPlanPreview.d.ts.map +1 -0
  169. package/dist/components/prompt-actions/PromptPlanPreview.js +12 -0
  170. package/dist/components/prompt-actions/PromptPlanPreview.js.map +1 -0
  171. package/dist/components/reorder/ReorderAnnouncer.d.ts +18 -0
  172. package/dist/components/reorder/ReorderAnnouncer.d.ts.map +1 -0
  173. package/dist/components/reorder/ReorderAnnouncer.js +17 -0
  174. package/dist/components/reorder/ReorderAnnouncer.js.map +1 -0
  175. package/dist/components/reorder/useReorderList.d.ts +18 -0
  176. package/dist/components/reorder/useReorderList.d.ts.map +1 -0
  177. package/dist/components/reorder/useReorderList.js +17 -0
  178. package/dist/components/reorder/useReorderList.js.map +1 -0
  179. package/dist/components/rule-builder/RuleBuilder.d.ts +18 -0
  180. package/dist/components/rule-builder/RuleBuilder.d.ts.map +1 -0
  181. package/dist/components/rule-builder/RuleBuilder.js +17 -0
  182. package/dist/components/rule-builder/RuleBuilder.js.map +1 -0
  183. package/dist/components/sales-channel-picker/SalesChannelPicker.d.ts +19 -0
  184. package/dist/components/sales-channel-picker/SalesChannelPicker.d.ts.map +1 -0
  185. package/dist/components/sales-channel-picker/SalesChannelPicker.js +18 -0
  186. package/dist/components/sales-channel-picker/SalesChannelPicker.js.map +1 -0
  187. package/dist/components/scope-notice/ScopeNotice.d.ts +18 -0
  188. package/dist/components/scope-notice/ScopeNotice.d.ts.map +1 -0
  189. package/dist/components/scope-notice/ScopeNotice.js +17 -0
  190. package/dist/components/scope-notice/ScopeNotice.js.map +1 -0
  191. package/dist/components/ui/alert.d.ts +18 -0
  192. package/dist/components/ui/alert.d.ts.map +1 -0
  193. package/dist/components/ui/alert.js +17 -0
  194. package/dist/components/ui/alert.js.map +1 -0
  195. package/dist/components/ui/badge.d.ts +18 -0
  196. package/dist/components/ui/badge.d.ts.map +1 -0
  197. package/dist/components/ui/badge.js +17 -0
  198. package/dist/components/ui/badge.js.map +1 -0
  199. package/dist/components/ui/button.d.ts +18 -0
  200. package/dist/components/ui/button.d.ts.map +1 -0
  201. package/dist/components/ui/button.js +17 -0
  202. package/dist/components/ui/button.js.map +1 -0
  203. package/dist/components/ui/card.d.ts +17 -0
  204. package/dist/components/ui/card.d.ts.map +1 -0
  205. package/dist/components/ui/card.js +17 -0
  206. package/dist/components/ui/card.js.map +1 -0
  207. package/dist/components/ui/checkbox.d.ts +17 -0
  208. package/dist/components/ui/checkbox.d.ts.map +1 -0
  209. package/dist/components/ui/checkbox.js +17 -0
  210. package/dist/components/ui/checkbox.js.map +1 -0
  211. package/dist/components/ui/collapsible.d.ts +5 -0
  212. package/dist/components/ui/collapsible.d.ts.map +1 -0
  213. package/dist/components/ui/collapsible.js +5 -0
  214. package/dist/components/ui/collapsible.js.map +1 -0
  215. package/dist/components/ui/color-picker.d.ts +17 -0
  216. package/dist/components/ui/color-picker.d.ts.map +1 -0
  217. package/dist/components/ui/color-picker.js +17 -0
  218. package/dist/components/ui/color-picker.js.map +1 -0
  219. package/dist/components/ui/combobox.d.ts +18 -0
  220. package/dist/components/ui/combobox.d.ts.map +1 -0
  221. package/dist/components/ui/combobox.js +17 -0
  222. package/dist/components/ui/combobox.js.map +1 -0
  223. package/dist/components/ui/input.d.ts +17 -0
  224. package/dist/components/ui/input.d.ts.map +1 -0
  225. package/dist/components/ui/input.js +17 -0
  226. package/dist/components/ui/input.js.map +1 -0
  227. package/dist/components/ui/label.d.ts +17 -0
  228. package/dist/components/ui/label.d.ts.map +1 -0
  229. package/dist/components/ui/label.js +17 -0
  230. package/dist/components/ui/label.js.map +1 -0
  231. package/dist/components/ui/multi-select.d.ts +18 -0
  232. package/dist/components/ui/multi-select.d.ts.map +1 -0
  233. package/dist/components/ui/multi-select.js +17 -0
  234. package/dist/components/ui/multi-select.js.map +1 -0
  235. package/dist/components/ui/page-header.d.ts +18 -0
  236. package/dist/components/ui/page-header.d.ts.map +1 -0
  237. package/dist/components/ui/page-header.js +17 -0
  238. package/dist/components/ui/page-header.js.map +1 -0
  239. package/dist/components/ui/route-tabs.d.ts +18 -0
  240. package/dist/components/ui/route-tabs.d.ts.map +1 -0
  241. package/dist/components/ui/route-tabs.js +17 -0
  242. package/dist/components/ui/route-tabs.js.map +1 -0
  243. package/dist/components/ui/save-button-group.d.ts +18 -0
  244. package/dist/components/ui/save-button-group.d.ts.map +1 -0
  245. package/dist/components/ui/save-button-group.js +17 -0
  246. package/dist/components/ui/save-button-group.js.map +1 -0
  247. package/dist/components/ui/select.d.ts +17 -0
  248. package/dist/components/ui/select.d.ts.map +1 -0
  249. package/dist/components/ui/select.js +17 -0
  250. package/dist/components/ui/select.js.map +1 -0
  251. package/dist/components/ui/separator.d.ts +17 -0
  252. package/dist/components/ui/separator.d.ts.map +1 -0
  253. package/dist/components/ui/separator.js +17 -0
  254. package/dist/components/ui/separator.js.map +1 -0
  255. package/dist/components/ui/table.d.ts +17 -0
  256. package/dist/components/ui/table.d.ts.map +1 -0
  257. package/dist/components/ui/table.js +17 -0
  258. package/dist/components/ui/table.js.map +1 -0
  259. package/dist/components/ui/textarea.d.ts +17 -0
  260. package/dist/components/ui/textarea.d.ts.map +1 -0
  261. package/dist/components/ui/textarea.js +17 -0
  262. package/dist/components/ui/textarea.js.map +1 -0
  263. package/dist/i18n/TranslationProvider.d.ts +18 -0
  264. package/dist/i18n/TranslationProvider.d.ts.map +1 -0
  265. package/dist/i18n/TranslationProvider.js +17 -0
  266. package/dist/i18n/TranslationProvider.js.map +1 -0
  267. package/dist/i18n/app-language-context.d.ts +18 -0
  268. package/dist/i18n/app-language-context.d.ts.map +1 -0
  269. package/dist/i18n/app-language-context.js +17 -0
  270. package/dist/i18n/app-language-context.js.map +1 -0
  271. package/dist/i18n/interpolate.d.ts +17 -0
  272. package/dist/i18n/interpolate.d.ts.map +1 -0
  273. package/dist/i18n/interpolate.js +17 -0
  274. package/dist/i18n/interpolate.js.map +1 -0
  275. package/dist/i18n/language-storage.d.ts +17 -0
  276. package/dist/i18n/language-storage.d.ts.map +1 -0
  277. package/dist/i18n/language-storage.js +17 -0
  278. package/dist/i18n/language-storage.js.map +1 -0
  279. package/dist/i18n/preauth-login-copy.d.ts +36 -0
  280. package/dist/i18n/preauth-login-copy.d.ts.map +1 -0
  281. package/dist/i18n/preauth-login-copy.js +36 -0
  282. package/dist/i18n/preauth-login-copy.js.map +1 -0
  283. package/dist/i18n/resolver.d.ts +18 -0
  284. package/dist/i18n/resolver.d.ts.map +1 -0
  285. package/dist/i18n/resolver.js +17 -0
  286. package/dist/i18n/resolver.js.map +1 -0
  287. package/dist/i18n/types.d.ts +17 -0
  288. package/dist/i18n/types.d.ts.map +1 -0
  289. package/dist/i18n/types.js +2 -0
  290. package/dist/i18n/types.js.map +1 -0
  291. package/dist/i18n/useTranslation.d.ts +17 -0
  292. package/dist/i18n/useTranslation.d.ts.map +1 -0
  293. package/dist/i18n/useTranslation.js +17 -0
  294. package/dist/i18n/useTranslation.js.map +1 -0
  295. package/dist/index.d.ts +55 -0
  296. package/dist/index.d.ts.map +1 -0
  297. package/dist/index.js +51 -0
  298. package/dist/index.js.map +1 -0
  299. package/dist/lib/admin-actions/AdminActionsProvider.d.ts +29 -0
  300. package/dist/lib/admin-actions/AdminActionsProvider.d.ts.map +1 -0
  301. package/dist/lib/admin-actions/AdminActionsProvider.js +51 -0
  302. package/dist/lib/admin-actions/AdminActionsProvider.js.map +1 -0
  303. package/dist/lib/admin-actions/api.d.ts +10 -0
  304. package/dist/lib/admin-actions/api.d.ts.map +1 -0
  305. package/dist/lib/admin-actions/api.js +13 -0
  306. package/dist/lib/admin-actions/api.js.map +1 -0
  307. package/dist/lib/admin-actions/icon-map.d.ts +17 -0
  308. package/dist/lib/admin-actions/icon-map.d.ts.map +1 -0
  309. package/dist/lib/admin-actions/icon-map.js +17 -0
  310. package/dist/lib/admin-actions/icon-map.js.map +1 -0
  311. package/dist/lib/admin-actions/useAdminActions.d.ts +17 -0
  312. package/dist/lib/admin-actions/useAdminActions.d.ts.map +1 -0
  313. package/dist/lib/admin-actions/useAdminActions.js +34 -0
  314. package/dist/lib/admin-actions/useAdminActions.js.map +1 -0
  315. package/dist/lib/api-client.d.ts +17 -0
  316. package/dist/lib/api-client.d.ts.map +1 -0
  317. package/dist/lib/api-client.js +17 -0
  318. package/dist/lib/api-client.js.map +1 -0
  319. package/dist/lib/auth.d.ts +18 -0
  320. package/dist/lib/auth.d.ts.map +1 -0
  321. package/dist/lib/auth.js +17 -0
  322. package/dist/lib/auth.js.map +1 -0
  323. package/dist/lib/federated-sign-in/api.d.ts +4 -0
  324. package/dist/lib/federated-sign-in/api.d.ts.map +1 -0
  325. package/dist/lib/federated-sign-in/api.js +38 -0
  326. package/dist/lib/federated-sign-in/api.js.map +1 -0
  327. package/dist/lib/federated-sign-in/index.d.ts +4 -0
  328. package/dist/lib/federated-sign-in/index.d.ts.map +1 -0
  329. package/dist/lib/federated-sign-in/index.js +4 -0
  330. package/dist/lib/federated-sign-in/index.js.map +1 -0
  331. package/dist/lib/federated-sign-in/resolve.d.ts +47 -0
  332. package/dist/lib/federated-sign-in/resolve.d.ts.map +1 -0
  333. package/dist/lib/federated-sign-in/resolve.js +24 -0
  334. package/dist/lib/federated-sign-in/resolve.js.map +1 -0
  335. package/dist/lib/federated-sign-in/useFederatedSignIn.d.ts +15 -0
  336. package/dist/lib/federated-sign-in/useFederatedSignIn.d.ts.map +1 -0
  337. package/dist/lib/federated-sign-in/useFederatedSignIn.js +37 -0
  338. package/dist/lib/federated-sign-in/useFederatedSignIn.js.map +1 -0
  339. package/dist/lib/format.d.ts +17 -0
  340. package/dist/lib/format.d.ts.map +1 -0
  341. package/dist/lib/format.js +17 -0
  342. package/dist/lib/format.js.map +1 -0
  343. package/dist/lib/module-presence/index.d.ts +19 -0
  344. package/dist/lib/module-presence/index.d.ts.map +1 -0
  345. package/dist/lib/module-presence/index.js +18 -0
  346. package/dist/lib/module-presence/index.js.map +1 -0
  347. package/dist/lib/module-registry/index.d.ts +84 -0
  348. package/dist/lib/module-registry/index.d.ts.map +1 -0
  349. package/dist/lib/module-registry/index.js +104 -0
  350. package/dist/lib/module-registry/index.js.map +1 -0
  351. package/dist/lib/money.d.ts +17 -0
  352. package/dist/lib/money.d.ts.map +1 -0
  353. package/dist/lib/money.js +17 -0
  354. package/dist/lib/money.js.map +1 -0
  355. package/dist/lib/prompt-actions/api.d.ts +10 -0
  356. package/dist/lib/prompt-actions/api.d.ts.map +1 -0
  357. package/dist/lib/prompt-actions/api.js +41 -0
  358. package/dist/lib/prompt-actions/api.js.map +1 -0
  359. package/dist/lib/prompt-actions/usePromptRequest.d.ts +26 -0
  360. package/dist/lib/prompt-actions/usePromptRequest.d.ts.map +1 -0
  361. package/dist/lib/prompt-actions/usePromptRequest.js +113 -0
  362. package/dist/lib/prompt-actions/usePromptRequest.js.map +1 -0
  363. package/dist/lib/surface-visibility.d.ts +15 -0
  364. package/dist/lib/surface-visibility.d.ts.map +1 -0
  365. package/dist/lib/surface-visibility.js +14 -0
  366. package/dist/lib/surface-visibility.js.map +1 -0
  367. package/dist/lib/text-normalization.d.ts +17 -0
  368. package/dist/lib/text-normalization.d.ts.map +1 -0
  369. package/dist/lib/text-normalization.js +17 -0
  370. package/dist/lib/text-normalization.js.map +1 -0
  371. package/dist/lib/use-page-size-preference.d.ts +16 -0
  372. package/dist/lib/use-page-size-preference.d.ts.map +1 -0
  373. package/dist/lib/use-page-size-preference.js +15 -0
  374. package/dist/lib/use-page-size-preference.js.map +1 -0
  375. package/dist/lib/use-unsaved-changes-prompt.d.ts +17 -0
  376. package/dist/lib/use-unsaved-changes-prompt.d.ts.map +1 -0
  377. package/dist/lib/use-unsaved-changes-prompt.js +17 -0
  378. package/dist/lib/use-unsaved-changes-prompt.js.map +1 -0
  379. package/dist/lib/utils.d.ts +17 -0
  380. package/dist/lib/utils.d.ts.map +1 -0
  381. package/dist/lib/utils.js +17 -0
  382. package/dist/lib/utils.js.map +1 -0
  383. package/dist/lib/uuid.d.ts +17 -0
  384. package/dist/lib/uuid.d.ts.map +1 -0
  385. package/dist/lib/uuid.js +17 -0
  386. package/dist/lib/uuid.js.map +1 -0
  387. package/dist/modules/home/HomePage.d.ts +3 -0
  388. package/dist/modules/home/HomePage.d.ts.map +1 -0
  389. package/dist/modules/home/HomePage.js +137 -0
  390. package/dist/modules/home/HomePage.js.map +1 -0
  391. package/dist/modules/home/RecentActivityCard.d.ts +3 -0
  392. package/dist/modules/home/RecentActivityCard.d.ts.map +1 -0
  393. package/dist/modules/home/RecentActivityCard.js +94 -0
  394. package/dist/modules/home/RecentActivityCard.js.map +1 -0
  395. package/dist/modules/home/activity-render.d.ts +69 -0
  396. package/dist/modules/home/activity-render.d.ts.map +1 -0
  397. package/dist/modules/home/activity-render.js +53 -0
  398. package/dist/modules/home/activity-render.js.map +1 -0
  399. package/dist/modules/home/useRecentActivity.d.ts +41 -0
  400. package/dist/modules/home/useRecentActivity.d.ts.map +1 -0
  401. package/dist/modules/home/useRecentActivity.js +57 -0
  402. package/dist/modules/home/useRecentActivity.js.map +1 -0
  403. package/dist/modules/platform/ModuleActivationControl.d.ts +28 -0
  404. package/dist/modules/platform/ModuleActivationControl.d.ts.map +1 -0
  405. package/dist/modules/platform/ModuleActivationControl.js +88 -0
  406. package/dist/modules/platform/ModuleActivationControl.js.map +1 -0
  407. package/dist/modules/platform/ModulesPage.d.ts +76 -0
  408. package/dist/modules/platform/ModulesPage.d.ts.map +1 -0
  409. package/dist/modules/platform/ModulesPage.js +175 -0
  410. package/dist/modules/platform/ModulesPage.js.map +1 -0
  411. package/dist/modules/platform/RecentActivityVisibilityControl.d.ts +38 -0
  412. package/dist/modules/platform/RecentActivityVisibilityControl.d.ts.map +1 -0
  413. package/dist/modules/platform/RecentActivityVisibilityControl.js +26 -0
  414. package/dist/modules/platform/RecentActivityVisibilityControl.js.map +1 -0
  415. package/dist/modules/platform/api.d.ts +27 -0
  416. package/dist/modules/platform/api.d.ts.map +1 -0
  417. package/dist/modules/platform/api.js +33 -0
  418. package/dist/modules/platform/api.js.map +1 -0
  419. package/dist/modules/profile/ProfilePage.d.ts +11 -0
  420. package/dist/modules/profile/ProfilePage.d.ts.map +1 -0
  421. package/dist/modules/profile/ProfilePage.js +140 -0
  422. package/dist/modules/profile/ProfilePage.js.map +1 -0
  423. package/dist/registerSw.d.ts +7 -0
  424. package/dist/registerSw.d.ts.map +1 -0
  425. package/dist/registerSw.js +20 -0
  426. package/dist/registerSw.js.map +1 -0
  427. package/package.json +74 -3
  428. package/tailwind.css +14 -0
@@ -0,0 +1,1259 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useFocusTrap } from './hooks/useFocusTrap.js';
4
+ import { BrandLogo } from './BrandLogo.js';
5
+ import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
6
+ import { Boxes, ChevronDown, ChevronRight, HelpCircle, Home as HomeIcon, Menu, LayoutDashboard, LogOut, PanelLeftClose, PanelLeftOpen, Search, Sparkles, } from 'lucide-react';
7
+ import { useAuth } from '../lib/auth.js';
8
+ import { cn } from '../lib/utils.js';
9
+ import { useTranslation } from '../i18n/useTranslation.js';
10
+ import { useTranslationContext } from '../i18n/TranslationProvider.js';
11
+ import { registryNavFor, useAdminRegistry, } from '../lib/module-registry/index.js';
12
+ import { LanguagePicker } from './LanguagePicker.js';
13
+ import { useViewportTier } from './hooks/useViewportTier.js';
14
+ import { NotificationBell } from './notifications/index.js';
15
+ import { useAdminActions } from '../lib/admin-actions/useAdminActions.js';
16
+ import { normalize } from '../lib/text-normalization.js';
17
+ import { useModulePresence } from '../lib/module-presence/index.js';
18
+ import { useSurfaceVisibility, } from '../lib/surface-visibility.js';
19
+ import { resolveIcon } from '../lib/admin-actions/icon-map.js';
20
+ import { getPromptCapability, listUnseenPromptRequests } from '../lib/prompt-actions/api.js';
21
+ import { PromptModePanel } from './prompt-actions/PromptModePanel.js';
22
+ import { SpeechToTextButton } from './SpeechToTextButton.js';
23
+ /* Design-aligned navigation. The "Home" + "Orders" cluster sits above
24
+ * the first labelled group exactly as the prototype shows. */
25
+ const NAV = [
26
+ {
27
+ key: 'main',
28
+ labelKey: '',
29
+ items: [
30
+ { to: '/', labelKey: 'appShell.nav.home', icon: LayoutDashboard, module: null },
31
+ ],
32
+ },
33
+ {
34
+ key: 'sales',
35
+ labelKey: 'appShell.section.sales',
36
+ items: [
37
+ // All three `/orders*` rows stood here until feature 091's Phase 4 batch
38
+ // 15; `orders` declares them now, at weights 100, 200 and 300 — the order
39
+ // they stood in — in `packages/modules/orders/src/admin/index.ts`. That
40
+ // *quick order is not a second destination but the other way of getting
41
+ // lines into the same order, so it lives behind a tab on the order-entry
42
+ // page* is still true and is recorded where the routes are.
43
+ //
44
+ // `/quote-requests`, `/invoices` and `/ksef` were declared here until
45
+ // feature 091's Phase 4 batch 12, at weights 400, 500 and 600 — the order
46
+ // they stood in here, and still the order they render in now that the
47
+ // three rows above them are contributions too.
48
+ //
49
+ // The section now holds **no host-declared row**, which has one
50
+ // operator-visible consequence recorded rather than glossed: the
51
+ // breadcrumb `registryCrumbs` derives for every screen in it renders
52
+ // *Sales* unlinked, because there is no host entry left for it to point
53
+ // at. That is batch 13's *Inventory* and *Pricing* arriving again, and it
54
+ // reaches the four contributed rows' trails as well as this module's own.
55
+ //
56
+ // `/ksef`'s glyph changes from `ReceiptText` to `Receipt`:
57
+ // `KnownIconNameSchema` does not carry the first, and `Receipt` is what
58
+ // this module's own `open-ksef` palette action has always named.
59
+ ],
60
+ },
61
+ {
62
+ key: 'catalog',
63
+ labelKey: 'appShell.section.catalog',
64
+ items: [
65
+ // All five `/catalog*` rows stood here until feature 091's Phase 4 batch
66
+ // 15; `catalog` declares them now, in
67
+ // `packages/modules/catalog/src/admin/index.ts`, at weights 10 to 50 —
68
+ // ten times the position rather than the usual hundred, because this
69
+ // section already holds three contributed rows and every one of them
70
+ // belongs *below* these five. `assets_library` declares 200, so
71
+ // `/catalog/categories` at 200 would tie with it and lose the tie-break
72
+ // on module id.
73
+ //
74
+ // The section now holds **no host-declared row**, so the derived
75
+ // breadcrumb renders *Catalog* unlinked — including on `pim_ergonode`'s
76
+ // five trails, whose own note recorded that crumb keeping its
77
+ // `/catalog/products` href while these rows were the host's.
78
+ //
79
+ // `/pim-ergonode` stood here until feature 091's Phase 4 batch 13.
80
+ // `pim_ergonode` declares it now, at weight 250 — between
81
+ // `assets_library`' 200 and `pim_pimcore`' 300, which is where both of
82
+ // those rows' own comments already said this one belonged and could not
83
+ // be while it was the host's, `composeNav` appending every registry entry
84
+ // after every host one. The reasoning that keeps it *one* row for five
85
+ // screens — the other four are reached through the tab strip on the
86
+ // connector page — is recorded where the routes are, in
87
+ // `packages/modules/pim_ergonode/src/admin/index.ts`.
88
+ //
89
+ // Feature 068's placement note stands and is why the section is *Catalog*
90
+ // rather than *Channels*: it is where catalogue content comes *from*, and
91
+ // the three surfaces it writes are its neighbours here.
92
+ ],
93
+ },
94
+ {
95
+ key: 'inventory',
96
+ labelKey: 'appShell.section.inventory',
97
+ items: [
98
+ // All five rows of this section stood here until feature 091's Phase 4
99
+ // batch 13; `inventory` declares them now, at weights 100 to 500 — the
100
+ // order they stood in — in
101
+ // `packages/modules/inventory/src/admin/index.ts`. The codes those
102
+ // declarations carry are the ones this table settled on 2026-08-29, when
103
+ // every one of the five stopped naming a module that owns none of the
104
+ // data behind them (`orders:read` for four screens about warehouses and
105
+ // stock, `catalog:write` for the CSV importer), and the importer keeps
106
+ // the `inventory:write` that separates it from the other four.
107
+ //
108
+ // The section now holds **no host-declared row**, which has one
109
+ // operator-visible consequence recorded rather than glossed: the
110
+ // breadcrumb `registryCrumbs` derives for these screens renders
111
+ // *Inventory* unlinked, because there is no host entry left for it to
112
+ // point at. That is batch 11's *Messaging* arriving a second time.
113
+ ],
114
+ },
115
+ {
116
+ key: 'pricing',
117
+ labelKey: 'appShell.section.pricing',
118
+ items: [
119
+ // `/price-lists` stood here until feature 091's Phase 4 batch 13.
120
+ // `price_lists` declares it now, at weight 100 — first in this section,
121
+ // which is where the operator has always seen it, and which is the whole
122
+ // of what keeps it above `taxes`' and `promotions`' 200 now that the host
123
+ // declares no row here at all. The same *Inventory* consequence applies:
124
+ // the derived breadcrumb renders *Pricing* unlinked.
125
+ //
126
+ // Carrier / payment gateway settings are reached as integrations from the
127
+ // Delivery methods / Payment methods pages (not top-level sidebar).
128
+ ],
129
+ },
130
+ {
131
+ key: 'customers',
132
+ labelKey: 'appShell.section.customers',
133
+ items: [
134
+ // `/customers`, `/customers/online` and `/organizations` stood here until
135
+ // feature 091's Phase 4 batch 14; `/comparisons` left in batch 6,
136
+ // `/customer-groups` in batch 7 and `/credit-limits` in batch 8. Every
137
+ // row in this section is the registry's now, ordered by the weights the
138
+ // four modules declare — 100, 200, 300, then 400 twice and 600 — which is
139
+ // the order the operator already had.
140
+ //
141
+ // **The any-of gate travels with the row.** `/organizations` is gated by
142
+ // `requireAdminAny(['customers:read', 'customers:manage'])`
143
+ // (`packages/modules/organizations/src/backend/routes.admin.ts:169`), and
144
+ // naming only the read code — as that entry did until 2026-08-29 — hid
145
+ // the screen from a role holding just `customers:manage`.
146
+ // `AdminNavDeclaration.requiredPermission` takes a `PermissionRequirement`
147
+ // for exactly this, so the array is declared unchanged in
148
+ // `packages/modules/organizations/src/admin/index.ts`.
149
+ //
150
+ // The section now holds **no host-declared row**, which has one
151
+ // operator-visible consequence recorded rather than glossed: the
152
+ // breadcrumb `registryCrumbs` derives for these screens renders
153
+ // *Customers* unlinked, because there is no host entry left for it to
154
+ // point at. That is batch 13's *Inventory* and *Pricing* arriving again.
155
+ ],
156
+ },
157
+ {
158
+ key: 'channels',
159
+ labelKey: 'appShell.section.channels',
160
+ items: [
161
+ // `/sales-channels` stood here until feature 091's Phase 4 batch 14;
162
+ // `/dictionary` and `/admin/dictionaries/audit` left in batch 10 and
163
+ // `/seo` in batch 8. Every row in this section is the registry's now, and
164
+ // `sales_channels` declares weight 100 against `dictionaries`' 200 and
165
+ // 300 and `seo`'s 400 — which is the order this table gave them while the
166
+ // first row was the host's and `composeNav` appended the rest. The
167
+ // *Customers* consequence above applies here too: the section holds no
168
+ // host-declared row, so the derived breadcrumb renders *Channels*
169
+ // unlinked.
170
+ ],
171
+ },
172
+ {
173
+ key: 'content',
174
+ labelKey: 'appShell.section.content',
175
+ items: [
176
+ // All seven rows of this section stood here until feature 091's Phase 4
177
+ // batch 16 — the four `/cms/…` and the three `/blog/…` — and `/megamenu`
178
+ // left in batch 8. `cms` and `blog` declare theirs now, at weights 100 to
179
+ // 400 and 600 to 800, in
180
+ // `packages/modules/{cms,blog}/src/admin/index.ts`. `megamenu` has
181
+ // declared 500 since batch 8 with a comment saying it sits after the four
182
+ // `/cms/…` rows and before the three `/blog/…` ones; it could not while
183
+ // both neighbours were the host's and `composeNav` appended every
184
+ // registry entry after every host one, and those seven weights are what
185
+ // make it true again.
186
+ //
187
+ // The section now holds **no host-declared row**, with the one
188
+ // operator-visible consequence every emptied section has: the breadcrumb
189
+ // `registryCrumbs` derives for these screens renders *Content* unlinked,
190
+ // because there is no host entry left for it to point at. That is batches
191
+ // 13's *Inventory* and *Pricing* and 14's *Customers* and *Channels*
192
+ // arriving one last time.
193
+ ],
194
+ },
195
+ {
196
+ key: 'messaging',
197
+ labelKey: 'appShell.section.messaging',
198
+ // **Empty on purpose** (feature 091, Phase 4 batch 11), which is
199
+ // `analyticsAds`' shape below. All three entries — `/transactional-emails`
200
+ // and its blocks and templates screens — are declared by
201
+ // `transactional_emails` and arrive through `composeNav` from
202
+ // `modules.generated.ts`. The section stays here because the shell owns the
203
+ // section taxonomy: `composeNav` refuses a contribution naming a section
204
+ // this file does not declare.
205
+ //
206
+ // With no host-declared entry left, the weights the module declares (100,
207
+ // 200, 300) are the whole of this section's order and reproduce exactly the
208
+ // order this table had.
209
+ items: [],
210
+ },
211
+ {
212
+ key: 'newsletter',
213
+ labelKey: 'appShell.section.newsletter',
214
+ // Empty on purpose, for the reason above and in the same batch: all six
215
+ // entries are `newsletter`'s, declared at weights 100 through 600, which is
216
+ // the order they stood in here.
217
+ items: [],
218
+ },
219
+ // Reporting and paid-acquisition surfaces. They were originally filed under
220
+ // System because each arrived as a lone integration, but together they are a
221
+ // domain an operator navigates to on purpose — not a system setting.
222
+ {
223
+ key: 'analyticsAds',
224
+ labelKey: 'appShell.section.analyticsAds',
225
+ // **Empty on purpose, and the first section that is** (feature 091, Phase 4
226
+ // batch three). All four of this section's entries — `/analytics`,
227
+ // `/google-analytics`, `/linkedin-ads`, `/meta-ads` — are now declared by
228
+ // the modules that own them and arrive through `composeNav` from
229
+ // `modules.generated.ts`. The section itself stays here because the shell
230
+ // owns the section taxonomy: `composeNav` refuses a contribution naming a
231
+ // section this file does not declare, since an entry in a section nobody
232
+ // renders is a surface that silently appears nowhere.
233
+ //
234
+ // The consequence is worth stating rather than discovering: with no
235
+ // host-declared entry left, `registryNavFor`'s `weight` is the whole of
236
+ // this section's order, and the weights the four modules declare (100, 200,
237
+ // 300, 400) reproduce exactly the order the hand-written table had. Batch
238
+ // one and batch two each recorded the append-to-the-end ordering as an
239
+ // operator-visible regression; this is where it is repaired.
240
+ items: [],
241
+ },
242
+ {
243
+ key: 'system',
244
+ labelKey: 'appShell.section.system',
245
+ items: [
246
+ // `module: null`, deliberately (D-36). This is the screen that switches
247
+ // modules on and off; attributing it to a module would mean the surface
248
+ // could switch itself out of existence, and nothing would be left to
249
+ // switch it back. It is gated on a permission, never on presence.
250
+ {
251
+ to: '/platform/modules',
252
+ labelKey: 'appShell.nav.platformModules',
253
+ icon: Boxes,
254
+ requiredPermission: 'platform.modules.read',
255
+ module: null,
256
+ },
257
+ // `/admin-users`, `/admin-roles` and `/audit-log` are declared by the
258
+ // modules that own them since feature 091's Phase 4 batch four, and
259
+ // arrive through `composeNav` from `modules.generated.ts` — including the
260
+ // split this file used to carry a comment about: the roles row is
261
+ // `admin_roles`' while the screen it points at is `admin_users`'. Their
262
+ // declarations, and that comment's reasoning, are in
263
+ // `packages/modules/{admin_users,admin_roles,audit_logs}/src/admin/index.ts`.
264
+ //
265
+ // Unlike *Analytics & Ads*, this section still holds host entries, so a
266
+ // registry entry appends after every one of them whatever its weight says
267
+ // — the operator-visible consequence the batch records.
268
+ // `/catalog/bulk-operations` stood here until feature 091's Phase 4 batch
269
+ // 15; `catalog` declares it now, at weight 400 — its position in this
270
+ // table's original thirteen rows, which is behind `/admin-roles`' 300 and
271
+ // ahead of `/custom-fields`' 500. One operator-visible change follows and
272
+ // is recorded rather than glossed: the row rendered *second* while it was
273
+ // the host's, `composeNav` putting every host row above every contributed
274
+ // one, and it renders fourth now — back where the table that wrote it put
275
+ // it. That *bulk operations may span many domains, so the entry lives
276
+ // under System while the URL stays `/catalog/bulk-operations` to keep
277
+ // existing deep links valid* is recorded where the route is.
278
+ // `/api-keys` and `/webhooks` are declared by the modules that own them
279
+ // since feature 091's Phase 4 (the plan's batch 6), and arrive through
280
+ // `composeNav` from `modules.generated.ts`. Their declarations are in
281
+ // `packages/modules/{api_keys,webhooks}/src/admin/index.ts`.
282
+ // `/credentials`, `/settings`, `/settings/groups` and `/settings/cache`
283
+ // are declared by the modules that own them since feature 091's Phase 4
284
+ // batch 10, and arrive through `composeNav` from `modules.generated.ts`.
285
+ // Their declarations are in
286
+ // `packages/modules/{credentials,settings}/src/admin/index.ts`.
287
+ //
288
+ // `/settings/pwa` is `pwa`'s and has been since batch six, when only the
289
+ // advertisement moved because the screen lived under
290
+ // `modules/settings/pages/`. Batch 10 moved that directory, so the screen
291
+ // went with it and the route is `pwa`'s too — the split closes rather
292
+ // than standing.
293
+ ],
294
+ },
295
+ ];
296
+ const STORAGE_KEY = 'b2b-admin.nav.collapsed-groups';
297
+ const RAIL_STORAGE_KEY = 'b2b-admin.nav.rail-mode';
298
+ /**
299
+ * Rail (icon-only) mode persistence. The whole sidebar shrinks to a
300
+ * 64px-wide column showing just icons; section labels disappear and
301
+ * each section becomes a single icon (the first item's icon). Hovering
302
+ * or clicking the icon reveals a popover with the section's items.
303
+ */
304
+ function loadRailMode() {
305
+ if (typeof window === 'undefined')
306
+ return false;
307
+ try {
308
+ return window.localStorage.getItem(RAIL_STORAGE_KEY) === '1';
309
+ }
310
+ catch {
311
+ return false;
312
+ }
313
+ }
314
+ function persistRailMode(value) {
315
+ if (typeof window === 'undefined')
316
+ return;
317
+ try {
318
+ window.localStorage.setItem(RAIL_STORAGE_KEY, value ? '1' : '0');
319
+ }
320
+ catch {
321
+ /* ignore */
322
+ }
323
+ }
324
+ function loadCollapsed() {
325
+ if (typeof window === 'undefined')
326
+ return new Set();
327
+ try {
328
+ const raw = window.localStorage.getItem(STORAGE_KEY);
329
+ if (!raw)
330
+ return new Set();
331
+ const parsed = JSON.parse(raw);
332
+ if (Array.isArray(parsed)) {
333
+ return new Set(parsed.filter((v) => typeof v === 'string'));
334
+ }
335
+ }
336
+ catch {
337
+ /* ignore */
338
+ }
339
+ return new Set();
340
+ }
341
+ function persistCollapsed(value) {
342
+ if (typeof window === 'undefined')
343
+ return;
344
+ try {
345
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(value)));
346
+ }
347
+ catch {
348
+ /* ignore */
349
+ }
350
+ }
351
+ const CRUMB_DICT = [
352
+ { test: /^\/$/, build: () => [{ labelKey: 'appShell.nav.home', href: null }] },
353
+ // Six hand-written `/catalog*` trails stood here until feature 091's Phase 4
354
+ // batch 15, and the two `/catalog/bulk-operations*` screens never had one at
355
+ // all — both fell to the humanised-segment fallback. `registryCrumbs` derives
356
+ // all eight now, from the sidebar entries `catalog` declares. Three
357
+ // operator-visible changes, stated rather than glossed:
358
+ //
359
+ // - *Catalog* loses its link, this section holding no host-declared entry
360
+ // any more for `registryCrumbs` to point at (batch 13's *Inventory* and
361
+ // *Pricing*, arriving again).
362
+ // - `/catalog/products/:id` loses its *Editor* leaf and reads
363
+ // *Catalog → Products*, which is the shape every converted module's
364
+ // sub-screen already gets.
365
+ // - the two bulk-operation screens gain the label their sidebar row carries,
366
+ // under *System* rather than under *Catalog*, which is the section that row
367
+ // has declared since it was written.
368
+ // Seventeen hand-written trails stood here until feature 091's Phase 4 batch
369
+ // 13 — the five `/pim-ergonode*`, the four `/inventory*`, the three
370
+ // `/warehouses*`, the two `/price-lists*` — plus `/orders/quick-order`, which
371
+ // never had one and fell to the humanised-segment fallback.
372
+ // `registryCrumbs` derives all of them now, from the sidebar entries the four
373
+ // modules declare, which is what keeps a converted module's breadcrumb from
374
+ // being a third shared file to edit. Four operator-visible changes, stated
375
+ // rather than glossed:
376
+ //
377
+ // - *Inventory* and *Pricing* lose their link, because neither section holds
378
+ // a host-declared entry any more for `registryCrumbs` to point at (batch
379
+ // 11's *Messaging*, arriving again).
380
+ // - Ergonode's five trails were three crumbs deep and are two: the derived
381
+ // trail is *Catalog → <the entry's own label>*, and the leaf for the four
382
+ // sub-screens becomes *Ergonode PIM* linked to nothing, because a nav
383
+ // entry is a landing surface and these four have none of their own. The
384
+ // three `appShell.nav.pimErgonode*` keys and `appShell.crumb.importRun`
385
+ // that only these rules rendered are retired from `_i18n` with them.
386
+ // - `/warehouses/new` and `/warehouses/:id` lose their *New* and *Edit*
387
+ // leaves for the same reason, which is the shape every converted module's
388
+ // sub-screen already gets.
389
+ // - the *Catalog* crumb on the Ergonode trails keeps its `/catalog/products`
390
+ // href, that being the section's first host-declared entry — the same
391
+ // answer the hand-written rules gave.
392
+ // The two carrier trails — `/delivery-methods/dhl-parcel` and
393
+ // `/settings/inpost` — stood here until feature 134's wave 1 (FR-023, the
394
+ // extraction's per-wave prologue W1). They are the first two of the seven
395
+ // this table has left, and they retire for a reason the earlier batches did
396
+ // not have: **a host-owned crumb table may not declare a trail whose
397
+ // destination only a module declares.** Both routes are declared in
398
+ // `dhl_parcel`'s and `inpost`'s own admin layers and nowhere else, so this
399
+ // table was asserting a trail whose other end it cannot see — and a module
400
+ // that ships separately from this package takes its route declarations with
401
+ // it, at which point the rule describes nothing
402
+ // (`specs/conventions/module-admin-surfaces.md`).
403
+ // `registryCrumbs` derives both trails now. Three
404
+ // operator-visible changes, stated rather than glossed:
405
+ //
406
+ // - `/delivery-methods/dhl-parcel` loses its *DHL Parcel* leaf and reads
407
+ // *Pricing → Delivery methods*. Neither carrier contributes a sidebar
408
+ // entry and neither is given one — that is the carriers' own
409
+ // `plan.md` Ruling 1, and a nav row added to make a crumb derivable would
410
+ // be a product change bought for a breadcrumb — so there is no contributed
411
+ // row for `registryCrumbs` to build a leaf from. It is the shape every
412
+ // converted module's sub-screen already gets.
413
+ // - `/settings/inpost` reads *System → Settings*, and this one is **worse
414
+ // than it was**, not merely shorter. `registryCrumbs` takes the longest
415
+ // nav entry the path sits under, and `/settings/inpost` sits under
416
+ // `settings`' own `/settings` rather than under anything the carrier
417
+ // declares, so the trail names a screen the operator is not on. The route
418
+ // is `inpost`'s own declaration, and it is moot in any instance that does
419
+ // not install that module; the alternatives were giving `inpost` a sidebar
420
+ // row it has never had, or keeping a host rule whose destination only the
421
+ // module declares, and both are refused above.
422
+ // - three `_i18n` keys retire, not two: the carriers' own pair, and the
423
+ // shared **parent** key both trails named. That third one was kept out of
424
+ // batch 8's sweep *explicitly* to parent these two trails —
425
+ // `packages/modules/delivery_methods/src/admin/index.ts` says so, and says
426
+ // it retires when they do — and after this it has no renderer at all; the
427
+ // sidebar row resolves `nav.deliveryMethods.label` in `delivery_methods`'
428
+ // own namespace. None of the three is spelled here on purpose:
429
+ // `batch-eight-surfaces.module-owned-surface.test.tsx` asserts a retired
430
+ // key's **absence from this file's source**, so naming one in a comment
431
+ // about retiring it reds the assertion that proves it was retired.
432
+ // The two `/organizations*` trails stood here until feature 091's Phase 4
433
+ // batch 14, and the three `/customers*` screens never had one at all — they
434
+ // fell to the humanised-segment fallback. `registryCrumbs` derives all five
435
+ // now, from the sidebar entries the two modules declare. Two operator-visible
436
+ // changes, stated rather than glossed: the *Customers* crumb loses its link,
437
+ // that section holding no host-declared entry any more for `registryCrumbs`
438
+ // to point at (batch 13's *Inventory* and *Pricing*, arriving again), and
439
+ // `/organizations/:id` loses its *Detail* leaf — the shape every converted
440
+ // module's sub-screen already gets. The three `/customers*` screens gain the
441
+ // labels their sidebar rows already carry, where they had none.
442
+ // Five hand-written `/orders*` trails stood here until feature 091's Phase 4
443
+ // batch 15 — the roster, `/orders/new`, `/orders/quick-order`,
444
+ // `/orders/statuses` and the generic `/orders/:id`. `registryCrumbs` derives
445
+ // them from the three sidebar entries `orders` declares, and it gets the
446
+ // ordering property those five had by construction: it takes the **longest**
447
+ // matching entry, where this table took the first, which is why the three
448
+ // named sub-pages had to precede the generic rule here. Three
449
+ // operator-visible changes, stated rather than glossed:
450
+ //
451
+ // - *Orders* becomes *Sales*, unlinked, on every one of these trails: the
452
+ // derived trail names the section the module declares, and the section
453
+ // holds no host entry left to link to.
454
+ // - `/orders/:id` loses its *Detail* leaf and reads *Sales → Orders*, which
455
+ // is the shape every converted module's sub-screen already gets. The
456
+ // `appShell.crumb.detail` key nothing else rendered retires with it.
457
+ // - `/orders/quick-order` loses its *Quick order* leaf for the same reason
458
+ // and reads *Sales → Orders* too. `quick_order` declares a route and no
459
+ // sidebar entry (batch 13), so there is no contributed row to derive a
460
+ // leaf from; `appShell.nav.quickOrder` retires with the rule.
461
+ // The four `/invoices*` and `/quote-requests*` trails stood here until
462
+ // feature 091's Phase 4 batch 12. `registryCrumbs` derives all four now, out
463
+ // of the sidebar entries the two modules declare, and `/ksef` — which never
464
+ // had a trail at all and fell to the humanised-segment fallback — gains the
465
+ // label its sidebar row already carries. One operator-visible change, stated
466
+ // rather than glossed: these four named *Customers* as their section crumb
467
+ // and linked it to `/organizations`, while the sidebar rows they describe sit
468
+ // in *Sales*. The derived trail reads the section the module declares, so the
469
+ // crumb becomes *Sales* linked to `/orders` — the section's first host entry,
470
+ // which is what every hand-written trail in this table uses and what the
471
+ // sidebar has said all along.
472
+ // The two `/sales-channels*` trails stood here until feature 091's Phase 4
473
+ // batch 14. `registryCrumbs` derives both now, out of the sidebar entry
474
+ // `sales_channels` declares, with the same two changes the `/organizations`
475
+ // note above records: the *Channels* crumb loses its link and
476
+ // `/sales-channels/:code` loses its *Detail* leaf.
477
+ //
478
+ // The seven `/cms…` rules stood here until feature 091's Phase 4 batch 16,
479
+ // which is where the note above said they would retire. `registryCrumbs`
480
+ // derives ten of the eleven `/cms…` trails now, out of the four sidebar
481
+ // entries `cms` declares, and the seven `/blog/…` screens — which never had a
482
+ // hand-written trail at all and fell to the humanised-segment fallback — gain
483
+ // the labels their sidebar rows carry. Three operator-visible changes, stated
484
+ // rather than glossed:
485
+ //
486
+ // * the section crumb becomes **Content**, unlinked. These seven rules named
487
+ // *Channels* linked to `/sales-channels`, which was already the wrong
488
+ // section — the sidebar has put these rows under *Content* for as long as
489
+ // the section has existed — and `registryCrumbs` reads the section the
490
+ // module declares. It is unlinked for the reason every emptied section's
491
+ // is: there is no host-declared row left for it to point at;
492
+ // * the six editor screens lose their `Editor` leaf and render
493
+ // `Content / CMS Pages →`, which is the trail every converted module's
494
+ // sub-screen already gets;
495
+ // * `/cms` — the bare alias, which has no sidebar row and which
496
+ // `registryCrumbs` therefore does not match — falls to the
497
+ // humanised-segment fallback and renders a lone *cms*. It is the one trail
498
+ // of the eleven that is worse than it was. Repairing it would mean either a
499
+ // host `<Navigate>`, which moves a `cms` deep link into the admin's own
500
+ // registry and is the direction this feature reverses, or a second sidebar
501
+ // row for a path no operator is shown. Neither is worth an unadvertised
502
+ // alias, and `/dictionaries/audit` took the same answer in batch 10.
503
+ // `/dictionary` and the two audit spellings had a hand-written trail here
504
+ // until feature 091's Phase 4 batch 10. `registryCrumbs` derives them now,
505
+ // from `dictionaries`' own nav contribution — two crumbs where the audit
506
+ // trail built three, the third having been a literal *audit* leaf there is
507
+ // nothing to derive. The unadvertised `/dictionaries/audit` spelling keeps
508
+ // resolving as a route and falls to the humanised-segment trail, which is
509
+ // what an unadvertised alias gets.
510
+ // `/admin-users`, `/admin-roles` and `/audit-log` had a hand-written trail
511
+ // here until feature 091's Phase 4 batch four. They are the modules' own
512
+ // declarations now, so `registryCrumbs` derives the trail from the composed
513
+ // sidebar — the label and the section come off the nav contribution, which is
514
+ // what stops this third registry from being a fifth shared file every
515
+ // converted module has to edit. Two consequences, both operator-visible and
516
+ // both stated rather than discovered: the section crumb's href becomes
517
+ // *System*'s first host entry, `/platform/modules`, where these three trails
518
+ // named `/admin-users` by hand; and the leaf label resolves in the owning
519
+ // module's namespace rather than in `core`.
520
+ { test: /^\/platform\/modules\/?$/, build: () => [
521
+ { labelKey: 'appShell.section.system', href: '/admin-users' },
522
+ { labelKey: 'appShell.nav.platformModules', href: null },
523
+ ] },
524
+ // `/settings` and `/settings/groups` had a hand-written trail here until
525
+ // feature 091's Phase 4 batch 10, and `/settings/cache` and `/settings/pwa`
526
+ // never had one at all — both fell to the humanised-segment fallback.
527
+ // `registryCrumbs` derives all four now, so the two that had a trail keep it
528
+ // with the section link corrected (*System*'s first host row is
529
+ // `/platform/modules`, where these named `/admin-users` by hand — batch
530
+ // four's recorded change, arriving again) and the two that had none gain the
531
+ // label their sidebar row already carries.
532
+ { test: /^\/profile\/?$/, build: () => [
533
+ { labelKey: 'appShell.crumb.myProfile', href: null },
534
+ ] },
535
+ // The six `/transactional-emails*` trails stood here until feature 091's
536
+ // Phase 4 batch 11. `registryCrumbs` derives all six now, out of the sidebar
537
+ // entries `transactional_emails` declares — which is what keeps a converted
538
+ // module's breadcrumb from being a third shared file to edit. Two of them
539
+ // change: the fragment editors lose their `Editor` leaf and render
540
+ // `Messaging / Email Blocks →` instead, which is the trail every other
541
+ // converted module's sub-screen already gets, and the *Messaging* crumb loses
542
+ // its link because that section now holds no host-declared entry for
543
+ // `registryCrumbs` to point at.
544
+ ];
545
+ /**
546
+ * The trail for a screen a **module** contributes, derived from the composed
547
+ * sidebar (feature 091).
548
+ *
549
+ * `CRUMB_DICT` is a third host registry naming module routes by hand — this
550
+ * feature converts `App.tsx`'s and `AppShell.tsx`'s, and the plan does not
551
+ * cover this one. Deriving the leaf instead of adding an entry per module is
552
+ * what keeps a converted module's breadcrumb from being a fifth shared file to
553
+ * edit: the label and the section are already declared in the module's nav
554
+ * contribution, and the parent link is the section's own first host entry —
555
+ * exactly what every hand-written trail in this table uses.
556
+ *
557
+ * Returns `null` for a path no module claims, so `CRUMB_DICT` keeps precedence
558
+ * and the URL-segment fallback keeps the last word.
559
+ */
560
+ function registryCrumbs(pathname, composedNav) {
561
+ const path = pathname.replace(/\/+$/, '') || '/';
562
+ let best = null;
563
+ for (const section of composedNav) {
564
+ for (const item of section.items) {
565
+ const labelScope = item.labelScope;
566
+ if (labelScope === undefined)
567
+ continue;
568
+ // The entry's own destination, or anything beneath it. A module's second
569
+ // and third routes (`/x/new`, `/x/:id`) have no sidebar row of their own
570
+ // and never will — a nav entry is a landing surface — so matching only
571
+ // the exact `to` would drop every converted module's sub-screens onto the
572
+ // humanised-segment fallback below. The hand-written trails this table is
573
+ // draining already covered subpaths that way, with the same shape.
574
+ if (item.to !== path && !path.startsWith(`${item.to}/`))
575
+ continue;
576
+ // **The longest match wins, and it is first-match no longer** (feature
577
+ // 091, batch 10). Until this batch no module contributed both a parent
578
+ // route and a child of it, so "the first entry that matches" and "the
579
+ // entry that matches most" were the same answer. `settings` contributes
580
+ // `/settings`, `/settings/groups` and `/settings/cache` and `pwa`
581
+ // contributes `/settings/pwa`; under first-match, all four screens would
582
+ // have rendered *Settings* as their leaf, because `/settings` sorts
583
+ // ahead of the rest by weight. That is a trail that no longer names where
584
+ // the operator is, which is the whole of what a breadcrumb does.
585
+ //
586
+ // `CRUMB_DICT` gets the same property for free by listing its specific
587
+ // patterns above its general ones; this is that ordering rule, derived
588
+ // instead of maintained.
589
+ if (best !== null && best.item.to.length >= item.to.length)
590
+ continue;
591
+ best = { section, item, labelScope };
592
+ }
593
+ }
594
+ if (best === null)
595
+ return null;
596
+ const { section, item, labelScope } = best;
597
+ const parent = section.items.find((other) => other.labelScope === undefined);
598
+ const trail = [];
599
+ if (section.labelKey) {
600
+ trail.push({ labelKey: section.labelKey, href: parent?.to ?? null });
601
+ }
602
+ trail.push({
603
+ labelKey: item.labelKey,
604
+ labelScope,
605
+ href: item.to === path ? null : item.to,
606
+ });
607
+ return trail;
608
+ }
609
+ function buildCrumbs(pathname, composedNav) {
610
+ for (const entry of CRUMB_DICT) {
611
+ const match = pathname.match(entry.test);
612
+ if (match)
613
+ return entry.build(match);
614
+ }
615
+ const contributed = registryCrumbs(pathname, composedNav);
616
+ if (contributed !== null)
617
+ return contributed;
618
+ // Keep real path segments in hrefs; only humanize the visible label.
619
+ const segments = pathname.split('/').filter(Boolean);
620
+ return segments.map((segment, idx) => ({
621
+ literal: segment.replace(/-/g, ' '),
622
+ href: idx === segments.length - 1 ? null : `/${segments.slice(0, idx + 1).join('/')}`,
623
+ }));
624
+ }
625
+ /**
626
+ * The palette's one matching rule, for every group it renders.
627
+ *
628
+ * Issue #233. The dialog merges three indexes and used to match with two
629
+ * different rules: the server-fed Actions group filters through `normalize`
630
+ * (`lib/text-normalization.ts`, applied in `lib/admin-actions/useAdminActions.ts`),
631
+ * which folds diacritics *and* the stroked letters NFD leaves standing, while
632
+ * the static Navigate group and the pinned Assistant row used a bare
633
+ * `toLowerCase().includes()`. Typing `zamowienia` therefore found the Actions
634
+ * row for orders and not the Navigate row for the same screen — and Polish
635
+ * operators routinely type without diacritics, so half the palette silently
636
+ * stopped answering them. Issue #236 then found a fourth copy in the
637
+ * page-builder drawer, which is why the fold now lives at
638
+ * `lib/text-normalization.ts` and not under `lib/admin-actions/`.
639
+ *
640
+ * `normalize` is **imported, not re-implemented**: the whole defect was one
641
+ * question answered twice, and a copy is how the two answers drifted apart. It
642
+ * folds `ł` through an explicit stroked-letter map, which matters — `ł` has no
643
+ * NFD decomposition, so the obvious `normalize('NFD').replace(...)` one-liner
644
+ * leaves `płatności` unmatched by `platnosci`.
645
+ *
646
+ * **Both sides are folded.** Folding only the haystack breaks the operator who
647
+ * does type `zamówienia`; folding only the query breaks the one who does not.
648
+ * (Postel's Law — accept what was typed and normalise it.)
649
+ *
650
+ * `needle` must already be normalised; the caller folds the query once per
651
+ * keystroke rather than once per row. `normalize` trims, so a query with a
652
+ * leading space matches what the operator meant rather than nothing at all
653
+ * (issue #236).
654
+ */
655
+ function matchesQuery(needle, ...haystacks) {
656
+ return haystacks.some((haystack) => normalize(haystack).includes(needle));
657
+ }
658
+ /**
659
+ * The static Navigate group.
660
+ *
661
+ * **Every `requiredPermission` here is read from the backend route that gates
662
+ * the destination, not from the sidebar entry beside it.** Copying a neighbour
663
+ * is how a wrong code gets propagated twice, and several of these are not what
664
+ * the neighbourhood suggests:
665
+ *
666
+ * - `/inventory`, `/warehouses`, `/inventory/low-stock` and
667
+ * `/inventory/notifications` by `inventory:read`, and `/inventory/import` by
668
+ * `inventory:write` — the module's own codes since 2026-08-29. Until then
669
+ * all four reads said `orders:read` and the importer said `catalog:write`,
670
+ * which is how a merchandiser came to be able to delete a warehouse and
671
+ * anyone who could read orders came to be able to enumerate every warehouse
672
+ * address (`packages/modules/inventory/src/backend/routes.admin.ts`);
673
+ * - `/payment-methods` by `payment_methods:read` and `/delivery-methods` by
674
+ * `delivery_methods:read`, each its owner's own code since 2026-08-28. They
675
+ * look like a pair and are not one: the codes are separate, they are granted
676
+ * separately, and neither is `catalog:read`, which is what both said until
677
+ * the two modules stopped borrowing the catalogue's authority
678
+ * (`packages/modules/payment_methods/src/backend/routes.ts:125`,
679
+ * `packages/modules/delivery_methods/src/backend/routes.ts:106`);
680
+ * - `/api-keys` and `/webhooks` by one shared `integrations:manage`
681
+ * (`api_keys/routes.ts:20`, `webhooks/routes.ts:21`);
682
+ * - `/dictionary` and its audit view by `dictionary.write` — the module
683
+ * declares no read code, so reading the dictionary requires the write one
684
+ * (`dictionaries/routes.admin.ts:72`);
685
+ * - `/organizations` by `requireAdminAny(['customers:read',
686
+ * 'customers:manage'])`, which is why the field takes an any-of array.
687
+ *
688
+ * **Denied entries are hidden, not shown disabled** — the same treatment module
689
+ * absence already gets, and applied identically in the sidebar and in the
690
+ * dashboard's quick actions. Three reasons, in order of weight:
691
+ *
692
+ * 1. The Actions group in this very dialog already hides on permission;
693
+ * `AdminActionsService` filters server-side before the row is sent. Showing
694
+ * Navigate rows disabled would make one palette use two opposite treatments
695
+ * for two groups the operator does not distinguish — identical-looking rows
696
+ * that behave differently (Law of Similarity).
697
+ * 2. On an empty query the palette lists every Navigate entry. For a
698
+ * single-domain role that is a wall of unusable rows in the surface whose
699
+ * entire job is to shorten the path to the one thing you asked for (Hick).
700
+ * 3. Keyboard and pointer must reach the same conclusion. The Arrow/Enter loop
701
+ * below indexes `items` directly, so a disabled row would still be
702
+ * selectable by keyboard and `Enter` would navigate into a 403 — a
703
+ * disabled-row design would need `aria-disabled`, a linked reason and cursor
704
+ * skipping to be honest. Hiding makes the two paths identical by
705
+ * construction and leaves nothing for a screen reader to announce.
706
+ *
707
+ * The cost of hiding is real and is answered elsewhere: "you do not have access
708
+ * to this" is actionable in a way "this module is not installed" is not, so it
709
+ * belongs at the point of failure — the 403 an operator reaches by bookmark or
710
+ * deep link — rather than in an index the operator is scanning for something
711
+ * else. That screen is not part of this change; see the report.
712
+ */
713
+ const PALETTE_ITEMS = [
714
+ { group: 'Navigate', labelMode: 'key', label: 'appShell.nav.home', sub: 'appShell.palette.sub.dashboard', icon: HomeIcon, to: '/', keywords: 'home dashboard strona główna pulpit', module: null },
715
+ // The `/catalog/products`, `/catalog/categories` and `/catalog/attributes`
716
+ // rows left here in feature 091's Phase 4 batch 15, and they left by batch
717
+ // 10's route: `catalog` declared `new-product` and nothing that *opened* any
718
+ // of the three, so `open-products`, `open-categories` and `open-attributes`
719
+ // arrive with these rows' destinations, codes and keywords, and their labels
720
+ // and descriptions are the six strings the rows rendered, moved into that
721
+ // module's own bundle. The `/orders` row left in the same batch and was
722
+ // batch 12's case instead: `orders`' manifest already declared `open-orders`
723
+ // with the same destination and the same code, so the row was a second copy
724
+ // the server was never asked about and nothing replaces it.
725
+ // The `/inventory` and `/price-lists` rows left here in feature 091's Phase 4
726
+ // batch 13, and they left by the two routes batches 10 and 12 already
727
+ // established. `inventory` was batch 12's case: its manifest already declared
728
+ // `open-inventory` with the same destination and the same code, so the row
729
+ // was a second copy the server was never asked about and nothing replaces it.
730
+ // `price_lists` was batch 10's: it declared no action at all, so
731
+ // `open-price-lists` arrives with this row's destination, code and keywords,
732
+ // and its label and description are the two strings the row rendered, moved
733
+ // into that module's own bundle. Either way the advertisement is now the one
734
+ // surface the effective enabled-set filters.
735
+ // The `/organizations` row left here in feature 091's Phase 4 batch 14, and
736
+ // it left by batch 10's route: `organizations` declared no action at all, so
737
+ // `open-organizations` arrives with this row's destination and keywords, and
738
+ // its label and description are the two strings the row rendered, moved into
739
+ // that module's own bundle.
740
+ //
741
+ // **One operator-visible narrowing, recorded rather than glossed.** This row
742
+ // carried an any-of pair — `['customers:read', 'customers:manage']` — and
743
+ // `ModuleActionSchema.requiredPermission` is a single string
744
+ // (`packages/contracts/src/admin-actions.ts:205`), so the action names
745
+ // `customers:read`. A role holding only `customers:manage` therefore loses
746
+ // this palette entry and keeps the sidebar one, whose declaration takes the
747
+ // whole `PermissionRequirement` and keeps the pair. The screen stays
748
+ // reachable; only the second way of reaching it narrows. Widening the action
749
+ // schema would touch the palette resolver, the permission inventory and
750
+ // `check:action-route-permissions`, which is a change of its own and not a
751
+ // batch's.
752
+ // The `/quote-requests` row left here in feature 091's Phase 4 batch 12, and
753
+ // nothing replaced it: `quote_requests`' manifest already declares
754
+ // `open-rfq-inbox` with the same destination, the same code and the same
755
+ // keywords, so the row was a second copy of a declaration the server has been
756
+ // serving all along — and a copy the server was never asked about, which for
757
+ // a module an operator can withdraw means a palette that goes on advertising
758
+ // the screen after the withdrawal.
759
+ // The `/sales-channels` row left here in the same batch and by the same
760
+ // route. `sales_channels` declared `new-sales-channel` and nothing that
761
+ // *opened* the roster, so `open-sales-channels` arrives with this row's
762
+ // destination, code and keywords, and its label and description are the two
763
+ // strings the row rendered.
764
+ // The four `dictionaries`, `settings` and `credentials` rows left here in
765
+ // feature 091's Phase 4 batch 10. A hand-written palette row is a copy the
766
+ // server was never asked about — it went on advertising the screen after an
767
+ // operator withdrew the module — so each is a manifest **action** now, which
768
+ // is the surface the effective enabled-set filters. `settings` and
769
+ // `credentials` already declared theirs (`open-settings`, `open-credentials`,
770
+ // `new-credential`); `dictionaries` declares `open-dictionary` and
771
+ // `open-dictionary-audit` for the first time, with the destinations, codes
772
+ // and keywords these rows carried.
773
+ // Feature 020 — the Actions group is now sourced from the module
774
+ // registry via useAdminActions(); the previously-hardcoded "New
775
+ // product" and "Import products" entries are declared by the
776
+ // catalog and import_export module manifests respectively.
777
+ ];
778
+ /**
779
+ * `[...hostNav, ...registryNav]`, per section (feature 091, FR-010;
780
+ * `contracts/admin-registry.md` R11).
781
+ *
782
+ * Built once at module scope because the registry is a static import: a
783
+ * contribution set cannot change without a rebuild, and recomputing it per
784
+ * render would allocate a new array on every keystroke in the palette input.
785
+ *
786
+ * **A module's entries append to their section**, ordered among themselves by
787
+ * `weight` then module id. R11 describes the end state — the whole section
788
+ * weight-ordered — which needs a weight on all 97 host entries and is Story 3's
789
+ * to deliver as it drains them; inventing one per host entry now would be a
790
+ * hundred numbers nothing derives. The consequence is visible and worth
791
+ * stating: `/import-export` used to sit between Credentials and Settings in the
792
+ * System group and now sits at that group's end.
793
+ *
794
+ * **A module may not invent a section** (D-23): `AdminNavSectionNameSchema` is
795
+ * closed, and a contribution naming a section this shell does not declare
796
+ * renders nowhere — so the composition refuses it here rather than dropping it
797
+ * silently.
798
+ */
799
+ function composeNav(sections, entries) {
800
+ const declared = new Set(sections.map((section) => section.key));
801
+ for (const entry of entries) {
802
+ for (const item of entry.contributions.nav ?? []) {
803
+ if (declared.has(item.section))
804
+ continue;
805
+ throw new Error(`Module '${entry.moduleId}' contributes a sidebar entry in section '${item.section}', ` +
806
+ `which this shell does not declare. An entry in a section nobody renders is a ` +
807
+ `surface that silently appears nowhere.`);
808
+ }
809
+ }
810
+ return sections.map((section) => ({
811
+ ...section,
812
+ items: [
813
+ ...section.items,
814
+ ...registryNavFor(section.key, entries).map((item) => ({
815
+ to: item.to,
816
+ labelKey: item.labelKey,
817
+ labelScope: item.module,
818
+ icon: resolveIcon(item.icon),
819
+ // `exactOptionalPropertyTypes` — an ungated entry has no property,
820
+ // not a property holding `undefined`.
821
+ ...(item.requiredPermission === undefined
822
+ ? {}
823
+ : { requiredPermission: item.requiredPermission }),
824
+ module: item.module,
825
+ })),
826
+ ],
827
+ }));
828
+ }
829
+ /**
830
+ * The host's sections with every module's contributed entry folded in.
831
+ *
832
+ * A hook rather than a module constant (feature 110, T120), because the
833
+ * registry is now the admin project's value and arrives through
834
+ * `AdminRegistryProvider`: a constant evaluated at import time would have to
835
+ * read a file this package may not name. `useMemo` keys on the registry
836
+ * identity, which is a module-scope array in every real build, so the
837
+ * composition runs once per mount exactly as the constant did once per process
838
+ * — and `composeNav`'s refusal of an undeclared section now raises during
839
+ * render instead of during import, where a test can see which section it was.
840
+ */
841
+ function useComposedNav() {
842
+ const entries = useAdminRegistry();
843
+ return useMemo(() => composeNav(NAV, entries), [entries]);
844
+ }
845
+ /**
846
+ * `t` for a nav entry's label, in whichever scope the entry declares.
847
+ *
848
+ * The host's entries resolve in `core` and a module's in its own namespace, and
849
+ * both go through the one translation context — a second resolver would be the
850
+ * drift issue #230 removed from the visibility question, in the label.
851
+ */
852
+ function useNavLabel() {
853
+ const { t } = useTranslationContext();
854
+ return useCallback((item) => t(item.labelScope ?? 'core', item.labelKey), [t]);
855
+ }
856
+ export function AppShell() {
857
+ const { me, logout, hasPermission } = useAuth();
858
+ // The host's nav with every installed module's contributed entry folded in.
859
+ const composedNav = useComposedNav();
860
+ // Feature 073 — the effective enabled-set. A module that is off contributes
861
+ // no sidebar entry and no palette entry; neither surface recombines the two
862
+ // presence axes, because the server already did.
863
+ const { isPresent: isModulePresent } = useModulePresence();
864
+ const isVisible = useSurfaceVisibility();
865
+ const navigate = useNavigate();
866
+ const location = useLocation();
867
+ const [collapsed, setCollapsed] = useState(() => loadCollapsed());
868
+ const [railMode, setRailMode] = useState(() => loadRailMode());
869
+ const [paletteOpen, setPaletteOpen] = useState(false);
870
+ const [navDrawerOpen, setNavDrawerOpen] = useState(false);
871
+ const sidebarRef = useRef(null);
872
+ const viewportTier = useViewportTier();
873
+ const isMobile = viewportTier === 'mobile';
874
+ useFocusTrap(sidebarRef, isMobile && navDrawerOpen);
875
+ const toggleRailMode = useCallback(() => {
876
+ setRailMode((prev) => {
877
+ const next = !prev;
878
+ persistRailMode(next);
879
+ return next;
880
+ });
881
+ }, []);
882
+ const t = useTranslation('core');
883
+ const navLabel = useNavLabel();
884
+ // The raw, scope-taking resolver, for the breadcrumb: a crumb a module
885
+ // contributed resolves its label in that module's own namespace.
886
+ const { t: tScoped } = useTranslationContext();
887
+ const fullName = me ? `${me.adminUser.firstName} ${me.adminUser.lastName}`.trim() : '';
888
+ const role = me?.role?.name ?? 'Admin';
889
+ const toggleSection = useCallback((key) => {
890
+ setCollapsed((prev) => {
891
+ const next = new Set(prev);
892
+ if (next.has(key))
893
+ next.delete(key);
894
+ else
895
+ next.add(key);
896
+ persistCollapsed(next);
897
+ return next;
898
+ });
899
+ }, []);
900
+ // ⌘K opens the palette anywhere in the admin; ⌘B toggles the
901
+ // sidebar rail (matches the shadcn / VS Code shortcut convention).
902
+ useEffect(() => {
903
+ const onKey = (e) => {
904
+ // `!e.shiftKey` keeps ⌘K off Ctrl/⌘+Shift+K, which pages claim for
905
+ // their own in-page search (e.g. the Settings page's filter box).
906
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'k') {
907
+ e.preventDefault();
908
+ setPaletteOpen(true);
909
+ }
910
+ else if (!isMobile &&
911
+ (e.metaKey || e.ctrlKey) &&
912
+ e.key.toLowerCase() === 'b') {
913
+ e.preventDefault();
914
+ toggleRailMode();
915
+ }
916
+ else if (e.key === 'Escape' && navDrawerOpen) {
917
+ setNavDrawerOpen(false);
918
+ }
919
+ };
920
+ window.addEventListener('keydown', onKey);
921
+ return () => window.removeEventListener('keydown', onKey);
922
+ }, [toggleRailMode, isMobile, navDrawerOpen]);
923
+ useEffect(() => {
924
+ setNavDrawerOpen(false);
925
+ }, [location.pathname]);
926
+ const crumbs = useMemo(() => buildCrumbs(location.pathname, composedNav), [location.pathname, composedNav]);
927
+ const isCmsRoute = location.pathname.startsWith('/cms');
928
+ return (_jsxs("div", { className: cn('b2b-app-layout', isMobile ? null : railMode ? 'b2b-app-layout--rail' : 'b2b-app-layout--expanded'), children: [isMobile && navDrawerOpen ? (_jsx("button", { type: "button", className: "b2b-nav-drawer-backdrop", "aria-label": t('appShell.mobileMenu.close'), onClick: () => setNavDrawerOpen(false) })) : null, _jsxs("aside", { ref: sidebarRef, className: cn('b2b-sidebar', railMode && !isMobile && 'b2b-sidebar--rail', isMobile && navDrawerOpen && 'b2b-sidebar--drawer-open'), "aria-hidden": isMobile && !navDrawerOpen ? true : undefined, role: "navigation", "aria-label": t('appShell.mobileMenu.title'), children: [_jsxs(NavLink, { to: "/", end: true, className: "b2b-sidebar__brand", style: { textDecoration: 'none', color: 'inherit', cursor: 'pointer' }, "aria-label": t('appShell.brand.dashboardLink'), title: railMode ? t('appShell.brand.text') : undefined, children: [_jsx("span", { className: "b2b-sidebar__brand-logo", children: _jsx(BrandLogo, { label: t('appShell.brand.text') }) }), _jsxs("span", { className: "b2b-sidebar__brand-text", children: [t('appShell.brand.textPrimary'), ' ', _jsx("span", { className: "b2b-sidebar__brand-text-accent", children: t('appShell.brand.textAccent') })] })] }), _jsx("div", { className: "b2b-sidebar__search", children: railMode ? (_jsx("button", { type: "button", className: "b2b-sidebar__search-rail", onClick: () => setPaletteOpen(true), title: t('appShell.search.openPalette'), "aria-label": t('appShell.search.openPalette'), children: _jsx(Search, { size: 16 }) })) : (_jsxs(_Fragment, { children: [_jsx(Search, { size: 14, className: "b2b-sidebar__search-icon" }), _jsx("input", { className: "b2b-sidebar__search-input", placeholder: t('appShell.search.placeholder'), readOnly: true, onClick: () => setPaletteOpen(true), onFocus: () => setPaletteOpen(true) }), _jsx("span", { className: "b2b-sidebar__search-kbd", children: t('appShell.search.shortcutSymbol') })] })) }), _jsx("nav", { className: "b2b-sidebar__nav", children: composedNav.map((section) => {
929
+ const isOpen = !collapsed.has(section.key);
930
+ // Permission-gate every entry, then presence-gate it (feature 073 /
931
+ // FR-031). Sections with no remaining visible items fold away
932
+ // entirely, which is what makes a whole domain disappear when every
933
+ // module in it is off — the same behaviour restricted admins have
934
+ // always had.
935
+ //
936
+ // Issue #230 — the predicate is `isSurfaceVisible`, shared with the
937
+ // command palette and the dashboard quick actions. The palette used
938
+ // to run a second, weaker copy of it; a shared helper is what stops
939
+ // the three from disagreeing again.
940
+ const visibleItems = section.items.filter(isVisible);
941
+ if (visibleItems.length === 0)
942
+ return null;
943
+ // Feature 019 / 021 — section labels go through useTranslation('core').
944
+ // The empty-labelKey "main" cluster keeps no label; every other
945
+ // group resolves its declared `labelKey`. Polish strings live
946
+ // in packages/modules/_i18n/i18n/pl.json under the same key.
947
+ const translatedLabel = section.labelKey ? t(section.labelKey) : '';
948
+ // In rail mode the section reduces to one icon (the first
949
+ // visible item's icon). Hover or click reveals a popover
950
+ // listing every visible item in the section.
951
+ if (railMode) {
952
+ return (_jsx(RailSection, { section: section, visibleItems: visibleItems, translatedLabel: translatedLabel }, section.key));
953
+ }
954
+ return (_jsxs("div", { className: cn('b2b-sidebar__group', !isOpen && section.labelKey && 'b2b-sidebar__group--collapsed'), children: [section.labelKey ? (_jsxs("button", { type: "button", className: "b2b-sidebar__group-label", onClick: () => toggleSection(section.key), children: [_jsx(ChevronDown, { size: 11 }), _jsx("span", { children: translatedLabel })] })) : null, _jsx("div", { className: "b2b-sidebar__items", children: visibleItems.map((item) => {
955
+ const Icon = item.icon;
956
+ // Force exact-match (`end`) when another item in the same
957
+ // section nests under this item's path. Without this, a
958
+ // parent route like `/inventory` lights up alongside its
959
+ // children (`/inventory/low-stock`, `/inventory/import`,
960
+ // …) because NavLink's default match is prefix-based.
961
+ // The home link (`/`) is the canonical example and is
962
+ // covered by the same rule (every other item starts with
963
+ // `/`).
964
+ const hasNestedSibling = section.items.some((other) => other !== item &&
965
+ other.to.startsWith(item.to === '/' ? '/' : `${item.to}/`));
966
+ return (_jsxs(NavLink, { to: item.to, end: item.to === '/' || hasNestedSibling, className: ({ isActive }) => cn('b2b-nav-item', isActive && 'is-active'), children: [_jsx(Icon, { size: 16 }), _jsx("span", { style: { flex: 1 }, children: navLabel(item) })] }, item.to));
967
+ }) })] }, section.key));
968
+ }) }), me ? (_jsxs("div", { className: "b2b-sidebar__foot", children: [_jsxs(NavLink, { to: "/profile", className: "b2b-sidebar__foot-identity", title: railMode
969
+ ? `${fullName || me.adminUser.email} — ${t('appShell.profileMenu.profile')}`
970
+ : t('appShell.profileMenu.profile'), "aria-label": t('appShell.profileMenu.profile'), children: [_jsx("div", { className: "b2b-avatar", children: (fullName || me.adminUser.email).slice(0, 2).toUpperCase() }), _jsxs("div", { className: "meta", children: [_jsx("div", { className: "name", children: fullName || me.adminUser.email }), _jsx("div", { className: "role", children: role })] })] }), _jsx("button", { type: "button", className: "icon-btn", title: t('appShell.profileMenu.signOut'), "aria-label": t('appShell.profileMenu.signOut'), onClick: () => void logout(), children: _jsx(LogOut, { size: 14 }) })] })) : null, !isMobile ? (_jsxs("button", { type: "button", className: "b2b-sidebar__rail-toggle", onClick: toggleRailMode, title: railMode
971
+ ? (t('appShell.sidebarToggle.expand') || 'Expand sidebar (⌘B)')
972
+ : (t('appShell.sidebarToggle.collapse') || 'Collapse sidebar (⌘B)'), "aria-label": railMode
973
+ ? (t('appShell.sidebarToggle.expand') || 'Expand sidebar')
974
+ : (t('appShell.sidebarToggle.collapse') || 'Collapse sidebar'), "aria-pressed": railMode, children: [railMode ? _jsx(PanelLeftOpen, { size: 14 }) : _jsx(PanelLeftClose, { size: 14 }), !railMode ? (_jsx("span", { className: "b2b-sidebar__rail-toggle-label", children: t('appShell.sidebarToggle.collapse') || 'Collapse' })) : null] })) : null] }), _jsxs("div", { style: { minWidth: 0, display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { className: "b2b-topbar", children: [isMobile ? (_jsx("button", { type: "button", className: "b2b-topbar__menu-btn", "aria-label": t('appShell.mobileMenu.open'), "aria-expanded": navDrawerOpen, onClick: () => setNavDrawerOpen((open) => !open), children: _jsx(Menu, { size: 20 }) })) : null, _jsx("div", { className: isCmsRoute ? 'b2b-topbar__crumbs-inner b2b-page b2b-page--wide' : 'b2b-topbar__crumbs', children: crumbs.map((c, i) => (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [i > 0 ? _jsx(ChevronRight, { size: 12, className: "crumb-sep" }) : null, c.href ? (_jsx(NavLink, { to: c.href, className: "crumb-link", style: { color: 'inherit', textDecoration: 'none' }, children: c.labelKey !== undefined ? tScoped(c.labelScope ?? 'core', c.labelKey) : c.literal })) : (_jsx("span", { className: "crumb-cur", children: c.labelKey !== undefined ? tScoped(c.labelScope ?? 'core', c.labelKey) : c.literal }))] }, i))) }), _jsxs("div", { className: "b2b-topbar__actions", children: [_jsx("button", { type: "button", className: "b2b-topbar__icon-btn", title: t('appShell.search.openPalette'), "aria-label": t('appShell.search.openPalette'), onClick: () => setPaletteOpen(true), children: _jsx(Search, { size: 16 }) }), isModulePresent('admin_notifications') && hasPermission('admin:read') ? (_jsx(NotificationBell, {})) : null, _jsx("button", { type: "button", className: "b2b-topbar__icon-btn b2b-topbar__hide-mobile", title: t('appShell.topbar.help'), "aria-label": t('appShell.topbar.help'), children: _jsx(HelpCircle, { size: 16 }) }), _jsx("div", { style: { width: 1, height: 22, background: 'var(--border-color)', margin: '0 4px' } }), _jsx(LanguagePicker, {}), me ? (_jsx(NavLink, { to: "/profile", className: "b2b-avatar", title: `${fullName || me.adminUser.email} — ${t('appShell.profileMenu.profile')}`, "aria-label": t('appShell.profileMenu.profile'), style: { textDecoration: 'none', cursor: 'pointer' }, children: (fullName || me.adminUser.email).slice(0, 2).toUpperCase() })) : null] })] }), _jsxs("main", { className: "b2b-main", style: { flex: 1, overflow: 'auto' }, children: [_jsx("div", { className: "b2b-impersonation-slot", "data-impersonation-banner": true }), _jsx(Outlet, {})] })] }), _jsx(CommandPalette, { open: paletteOpen, onClose: () => setPaletteOpen(false), onNavigate: (to) => {
975
+ navigate(to);
976
+ setPaletteOpen(false);
977
+ } })] }));
978
+ }
979
+ function RailSection(props) {
980
+ const { section, visibleItems, translatedLabel } = props;
981
+ const navLabel = useNavLabel();
982
+ const [hover, setHover] = useState(false);
983
+ const [sticky, setSticky] = useState(false);
984
+ const wrapperRef = useRef(null);
985
+ const closeTimerRef = useRef(null);
986
+ const open = hover || sticky;
987
+ const location = useLocation();
988
+ // Active-state for the rail icon: light up when ANY visible item in
989
+ // the section matches the current URL (exact for the home route,
990
+ // prefix-with-segment-boundary for everything else).
991
+ const sectionActive = visibleItems.some((item) => {
992
+ if (item.to === '/')
993
+ return location.pathname === '/';
994
+ return (location.pathname === item.to ||
995
+ location.pathname.startsWith(`${item.to}/`));
996
+ });
997
+ const cancelClose = useCallback(() => {
998
+ if (closeTimerRef.current !== null) {
999
+ window.clearTimeout(closeTimerRef.current);
1000
+ closeTimerRef.current = null;
1001
+ }
1002
+ }, []);
1003
+ const scheduleClose = useCallback(() => {
1004
+ cancelClose();
1005
+ closeTimerRef.current = window.setTimeout(() => setHover(false), 120);
1006
+ }, [cancelClose]);
1007
+ // Click outside the wrapper closes the sticky popover.
1008
+ useEffect(() => {
1009
+ if (!sticky)
1010
+ return;
1011
+ const onDown = (e) => {
1012
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
1013
+ setSticky(false);
1014
+ }
1015
+ };
1016
+ window.addEventListener('mousedown', onDown);
1017
+ return () => window.removeEventListener('mousedown', onDown);
1018
+ }, [sticky]);
1019
+ useEffect(() => () => cancelClose(), [cancelClose]);
1020
+ const railSource = visibleItems[0] ?? section.items[0];
1021
+ if (!railSource)
1022
+ return null;
1023
+ const RailIcon = railSource.icon;
1024
+ const headerLabel = translatedLabel || (visibleItems[0] ? navLabel(visibleItems[0]) : '');
1025
+ // Single-item section: render a plain link with no popover. The
1026
+ // hover area is the link itself; tooltip carries the label.
1027
+ if (visibleItems.length === 1) {
1028
+ const only = visibleItems[0];
1029
+ const Icon = only.icon;
1030
+ const onlyLabel = navLabel(only);
1031
+ return (_jsx("div", { className: "b2b-sidebar__rail-row", children: _jsx(NavLink, { to: only.to, end: only.to === '/', className: ({ isActive }) => cn('b2b-nav-item b2b-nav-item--rail', isActive && 'is-active'), title: onlyLabel, "aria-label": onlyLabel, children: _jsx(Icon, { size: 18 }) }) }));
1032
+ }
1033
+ return (_jsxs("div", { ref: wrapperRef, className: "b2b-sidebar__rail-row", onMouseEnter: () => {
1034
+ cancelClose();
1035
+ setHover(true);
1036
+ }, onMouseLeave: scheduleClose, children: [_jsx("button", { type: "button", className: cn('b2b-nav-item b2b-nav-item--rail', sectionActive && 'is-active', open && 'is-open'), onClick: () => setSticky((prev) => !prev), "aria-haspopup": "menu", "aria-expanded": open, "aria-label": headerLabel, title: headerLabel, children: _jsx(RailIcon, { size: 18 }) }), open ? (_jsxs("div", { className: "b2b-sidebar__rail-popover", role: "menu", children: [headerLabel ? (_jsx("div", { className: "b2b-sidebar__rail-popover-header", children: headerLabel })) : null, visibleItems.map((item) => {
1037
+ const Icon = item.icon;
1038
+ const hasNestedSibling = visibleItems.some((other) => other !== item &&
1039
+ other.to.startsWith(item.to === '/' ? '/' : `${item.to}/`));
1040
+ return (_jsxs(NavLink, { to: item.to, end: item.to === '/' || hasNestedSibling, role: "menuitem", className: ({ isActive }) => cn('b2b-sidebar__rail-popover-item', isActive && 'is-active'), onClick: () => {
1041
+ setSticky(false);
1042
+ setHover(false);
1043
+ }, children: [_jsx(Icon, { size: 14 }), _jsx("span", { children: navLabel(item) })] }, item.to));
1044
+ })] })) : null] }));
1045
+ }
1046
+ /** Sentinel route for the assistant entry row — switches the palette mode
1047
+ * instead of navigating (feature 043; module_actions are navigation-only). */
1048
+ const PROMPT_MODE_SENTINEL = '__prompt_actions_mode__';
1049
+ /** Module-level capability cache: one fetch per session unless it failed. */
1050
+ let promptCapabilityCache = null;
1051
+ /** Test seam — interaction tests exercise multiple capability states. */
1052
+ export function resetPromptCapabilityCacheForTesting() {
1053
+ promptCapabilityCache = null;
1054
+ }
1055
+ function CommandPalette(props) {
1056
+ const { open, onClose, onNavigate } = props;
1057
+ const t = useTranslation('core');
1058
+ const tp = useTranslation('prompt_actions');
1059
+ const { hasPermission } = useAuth();
1060
+ const isVisible = useSurfaceVisibility();
1061
+ const [query, setQuery] = useState('');
1062
+ const [cursor, setCursor] = useState(0);
1063
+ const [mode, setMode] = useState('search');
1064
+ const [assistantReady, setAssistantReady] = useState(promptCapabilityCache === 'ready');
1065
+ const [unseen, setUnseen] = useState([]);
1066
+ const [initialRequest, setInitialRequest] = useState(undefined);
1067
+ // Seed text carried into prompt mode by the `/ai <command>` palette shortcut.
1068
+ const [initialPrompt, setInitialPrompt] = useState('');
1069
+ const inputRef = useRef(null);
1070
+ const { actions: registryActions } = useAdminActions(query);
1071
+ // Feature 043 — capability probe, only when the operator may use the
1072
+ // assistant. With the feature disabled/unconfigured (or the permission
1073
+ // missing) the palette renders exactly as before (FR-015/FR-020, SC-006).
1074
+ const mayUseAssistant = hasPermission('prompt_actions:use');
1075
+ useEffect(() => {
1076
+ if (!open || !mayUseAssistant || promptCapabilityCache !== null)
1077
+ return;
1078
+ void getPromptCapability()
1079
+ .then((cap) => {
1080
+ promptCapabilityCache = cap.status === 'ready' ? 'ready' : 'unavailable';
1081
+ setAssistantReady(promptCapabilityCache === 'ready');
1082
+ })
1083
+ .catch(() => {
1084
+ promptCapabilityCache = 'unavailable';
1085
+ });
1086
+ }, [open, mayUseAssistant]);
1087
+ // FR-018 — completion notice: prompts that finished while the palette was
1088
+ // closed surface on the next open until acknowledged.
1089
+ useEffect(() => {
1090
+ if (!open || !mayUseAssistant || !assistantReady) {
1091
+ setUnseen([]);
1092
+ return;
1093
+ }
1094
+ void listUnseenPromptRequests()
1095
+ .then(setUnseen)
1096
+ .catch(() => setUnseen([]));
1097
+ }, [open, mayUseAssistant, assistantReady]);
1098
+ // Map registry-supplied actions into the local PaletteItem shape so
1099
+ // the rendering loop stays uniform across Navigate (static) and
1100
+ // Actions (registry-driven) groups.
1101
+ const actionItems = useMemo(() => registryActions.map((a) => ({
1102
+ group: 'Actions',
1103
+ labelMode: 'literal',
1104
+ label: a.label,
1105
+ sub: a.description ?? '',
1106
+ icon: resolveIcon(a.icon),
1107
+ to: a.targetRoute,
1108
+ // Carried for shape uniformity only: an Actions row is filtered by
1109
+ // `useAdminActions(query)` before it reaches this mapper, so nothing
1110
+ // below reads this field. Kept unfolded — `matchesQuery` normalises
1111
+ // its haystack at compare time, and a second, differently-normalised
1112
+ // copy of the same data is what issue #233 was about.
1113
+ keywords: a.keywords.join(' '),
1114
+ })), [registryActions]);
1115
+ // Feature 073 / FR-032 plus issue #230 — the Navigate group, filtered on both
1116
+ // axes by the same predicate the sidebar uses. The Actions group needs nothing
1117
+ // here: the server already resolves both axes for it.
1118
+ const navigateItems = useMemo(() => PALETTE_ITEMS.filter(isVisible), [isVisible]);
1119
+ // Build a derived view that translates Navigate-group keys at render
1120
+ // time and leaves the registry-supplied Actions strings untouched.
1121
+ const resolveLabel = (it) => it.labelMode === 'key' ? t(it.label) : it.label;
1122
+ const resolveSub = (it) => it.labelMode === 'key' ? t(it.sub) : it.sub;
1123
+ // Feature 043 — pinned assistant entry (only when enabled + configured +
1124
+ // permitted). Always listed first; matched generously while typing.
1125
+ const assistantItems = useMemo(() => {
1126
+ if (!mayUseAssistant || !assistantReady)
1127
+ return [];
1128
+ return [
1129
+ {
1130
+ group: 'Assistant',
1131
+ labelMode: 'literal',
1132
+ label: tp('palette.entry.label'),
1133
+ sub: tp('palette.entry.description'),
1134
+ icon: Sparkles,
1135
+ to: PROMPT_MODE_SENTINEL,
1136
+ keywords: 'assistant ai prompt ask asystent zapytaj',
1137
+ },
1138
+ ];
1139
+ }, [mayUseAssistant, assistantReady, tp]);
1140
+ const items = useMemo(() => {
1141
+ if (!query.trim())
1142
+ return [...assistantItems, ...navigateItems, ...actionItems];
1143
+ // Folded once here, then compared against every row — see `matchesQuery`.
1144
+ const q = normalize(query);
1145
+ const filteredAssistant = assistantItems.filter((i) => matchesQuery(q, i.label, i.sub, i.keywords));
1146
+ // Navigate group: filter against the *translated* label + sub plus
1147
+ // the raw keywords list so a Polish user can search in Polish and
1148
+ // an English user in English.
1149
+ const filteredNav = navigateItems.filter((i) => matchesQuery(q, resolveLabel(i), resolveSub(i), i.keywords));
1150
+ return [...filteredAssistant, ...filteredNav, ...actionItems];
1151
+ // `resolveLabel` / `resolveSub` are render-scoped closures over `t`; the
1152
+ // list re-derives on every language change through the parent re-render.
1153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1154
+ }, [query, actionItems, assistantItems, navigateItems]);
1155
+ // Activating the assistant row switches the palette body instead of
1156
+ // navigating (feature 043); everything else keeps the navigation path.
1157
+ const activate = useCallback((to) => {
1158
+ if (to === PROMPT_MODE_SENTINEL) {
1159
+ setInitialPrompt('');
1160
+ setMode('prompt');
1161
+ return;
1162
+ }
1163
+ onNavigate(to);
1164
+ }, [onNavigate]);
1165
+ // `/ai` shortcut: typing `/ai` (optionally followed by a command) in the
1166
+ // palette input launches prompt mode directly, carrying any typed command
1167
+ // into the assistant input — equivalent to opening "Ask the assistant" and
1168
+ // typing it. Only intercepts when the assistant is actually usable.
1169
+ const handleQueryChange = useCallback((value) => {
1170
+ const match = /^\/ai(?:\s+(.*))?$/i.exec(value);
1171
+ if (match && mayUseAssistant && assistantReady) {
1172
+ setQuery('');
1173
+ setInitialPrompt(match[1] ?? '');
1174
+ setMode('prompt');
1175
+ return;
1176
+ }
1177
+ setQuery(value);
1178
+ }, [mayUseAssistant, assistantReady]);
1179
+ useEffect(() => {
1180
+ setCursor(0);
1181
+ }, [query, open]);
1182
+ useEffect(() => {
1183
+ if (!open) {
1184
+ setQuery('');
1185
+ setMode('search');
1186
+ setInitialRequest(undefined);
1187
+ setInitialPrompt('');
1188
+ }
1189
+ }, [open]);
1190
+ useEffect(() => {
1191
+ if (!open)
1192
+ return;
1193
+ if (mode === 'prompt') {
1194
+ // Prompt mode owns its own inputs; only Esc is handled here — it
1195
+ // returns to the classic palette rather than closing (research §R9).
1196
+ const onKey = (e) => {
1197
+ if (e.key === 'Escape') {
1198
+ e.stopPropagation();
1199
+ setMode('search');
1200
+ }
1201
+ };
1202
+ window.addEventListener('keydown', onKey);
1203
+ return () => window.removeEventListener('keydown', onKey);
1204
+ }
1205
+ inputRef.current?.focus();
1206
+ const onKey = (e) => {
1207
+ if (e.key === 'ArrowDown') {
1208
+ e.preventDefault();
1209
+ setCursor((c) => Math.min(items.length - 1, c + 1));
1210
+ }
1211
+ else if (e.key === 'ArrowUp') {
1212
+ e.preventDefault();
1213
+ setCursor((c) => Math.max(0, c - 1));
1214
+ }
1215
+ else if (e.key === 'Enter') {
1216
+ e.preventDefault();
1217
+ const it = items[cursor];
1218
+ if (it)
1219
+ activate(it.to);
1220
+ }
1221
+ else if (e.key === 'Escape') {
1222
+ onClose();
1223
+ }
1224
+ };
1225
+ window.addEventListener('keydown', onKey);
1226
+ return () => window.removeEventListener('keydown', onKey);
1227
+ }, [open, mode, items, cursor, onClose, activate]);
1228
+ if (!open)
1229
+ return null;
1230
+ if (mode === 'prompt') {
1231
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "b2b-scrim", onClick: onClose }), _jsx("div", { className: "b2b-palette", role: "dialog", "aria-modal": "true", children: _jsx(PromptModePanel, { onExit: () => {
1232
+ setInitialRequest(undefined);
1233
+ setInitialPrompt('');
1234
+ setMode('search');
1235
+ }, ...(initialRequest ? { initialRequest } : {}), ...(initialPrompt ? { initialPrompt } : {}) }) })] }));
1236
+ }
1237
+ let lastGroup = '';
1238
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "b2b-scrim", onClick: onClose }), _jsxs("div", { className: "b2b-palette", role: "dialog", "aria-modal": "true", children: [_jsxs("div", { className: "b2b-palette__input", children: [_jsx(Search, { size: 18, style: { color: 'var(--fg-muted)' } }), _jsx("input", { ref: inputRef, placeholder: t('appShell.search.commandPalettePlaceholder'), value: query, onChange: (e) => handleQueryChange(e.target.value) }), _jsx(SpeechToTextButton, { startTitle: t('appShell.search.voiceInput'), stopTitle: t('appShell.search.voiceInputStop'), onTranscript: (text) => {
1239
+ setQuery((q) => (q ? `${q} ${text}` : text));
1240
+ inputRef.current?.focus();
1241
+ } }), _jsx("span", { className: "b2b-kbd-sm", children: "esc" })] }), _jsxs("div", { className: "b2b-palette__list", children: [unseen.length > 0 ? (_jsxs("div", { "data-testid": "prompt-unseen-notice", className: "b2b-palette__item", style: { cursor: 'pointer' }, onClick: () => {
1242
+ setInitialRequest(unseen[0]);
1243
+ setUnseen((u) => u.slice(1));
1244
+ setMode('prompt');
1245
+ }, children: [_jsx(Sparkles, { size: 16 }), _jsx("div", { children: _jsx("div", { children: tp('panel.unseenNotice') }) })] })) : null, items.length === 0 ? (_jsx("div", { style: { padding: 32, textAlign: 'center', color: 'var(--fg-muted)', fontSize: 13 }, children: t('appShell.search.noMatches') })) : (items.map((it, i) => {
1246
+ const showGroup = it.group !== lastGroup;
1247
+ lastGroup = it.group;
1248
+ const Icon = it.icon;
1249
+ const groupHeader = it.group === 'Navigate'
1250
+ ? t('appShell.palette.group.navigate')
1251
+ : it.group === 'Assistant'
1252
+ ? tp('palette.group.label')
1253
+ : t('appShell.palette.group.actions');
1254
+ const label = resolveLabel(it);
1255
+ const sub = resolveSub(it);
1256
+ return (_jsxs("div", { children: [showGroup ? _jsx("div", { className: "b2b-palette__group", children: groupHeader }) : null, _jsxs("div", { className: cn('b2b-palette__item', i === cursor && 'is-cur'), onMouseEnter: () => setCursor(i), onClick: () => activate(it.to), children: [_jsx(Icon, { size: 16 }), _jsxs("div", { children: [_jsx("div", { children: label }), _jsx("div", { style: { fontSize: 11, color: 'var(--fg-muted)' }, children: sub })] }), _jsx("span", { className: "meta", children: i === cursor ? _jsx("span", { className: "b2b-kbd-sm", children: "\u21B5" }) : null })] })] }, `${it.group}-${it.label}`));
1257
+ }))] })] })] }));
1258
+ }
1259
+ //# sourceMappingURL=AppShell.js.map