@multiplatform.one/backoffice 6.3.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 (414) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +310 -0
  3. package/dist/cjs/chrome/BackofficeAwesomebar.cjs +95 -0
  4. package/dist/cjs/chrome/BackofficeAwesomebar.native.js +106 -0
  5. package/dist/cjs/chrome/BackofficeAwesomebar.native.js.map +1 -0
  6. package/dist/cjs/chrome/CommandPalette.cjs +453 -0
  7. package/dist/cjs/chrome/CommandPalette.native.js +505 -0
  8. package/dist/cjs/chrome/CommandPalette.native.js.map +1 -0
  9. package/dist/cjs/chrome/HelpMenu.cjs +131 -0
  10. package/dist/cjs/chrome/HelpMenu.native.js +145 -0
  11. package/dist/cjs/chrome/HelpMenu.native.js.map +1 -0
  12. package/dist/cjs/chrome/NotificationsButton.cjs +112 -0
  13. package/dist/cjs/chrome/NotificationsButton.native.js +115 -0
  14. package/dist/cjs/chrome/NotificationsButton.native.js.map +1 -0
  15. package/dist/cjs/chrome/UserMenu.cjs +158 -0
  16. package/dist/cjs/chrome/UserMenu.native.js +186 -0
  17. package/dist/cjs/chrome/UserMenu.native.js.map +1 -0
  18. package/dist/cjs/chrome/awesomebar-data.cjs +173 -0
  19. package/dist/cjs/chrome/awesomebar-data.native.js +233 -0
  20. package/dist/cjs/chrome/awesomebar-data.native.js.map +1 -0
  21. package/dist/cjs/chrome/command-palette-model.cjs +208 -0
  22. package/dist/cjs/chrome/command-palette-model.native.js +301 -0
  23. package/dist/cjs/chrome/command-palette-model.native.js.map +1 -0
  24. package/dist/cjs/chrome/help-about-dialog.cjs +136 -0
  25. package/dist/cjs/chrome/help-about-dialog.native.js +146 -0
  26. package/dist/cjs/chrome/help-about-dialog.native.js.map +1 -0
  27. package/dist/cjs/chrome/help-shortcuts-dialog.cjs +155 -0
  28. package/dist/cjs/chrome/help-shortcuts-dialog.native.js +168 -0
  29. package/dist/cjs/chrome/help-shortcuts-dialog.native.js.map +1 -0
  30. package/dist/cjs/chrome/notifications-data.cjs +190 -0
  31. package/dist/cjs/chrome/notifications-data.native.js +271 -0
  32. package/dist/cjs/chrome/notifications-data.native.js.map +1 -0
  33. package/dist/cjs/chrome/notifications-panel.cjs +171 -0
  34. package/dist/cjs/chrome/notifications-panel.native.js +189 -0
  35. package/dist/cjs/chrome/notifications-panel.native.js.map +1 -0
  36. package/dist/cjs/chrome/notifications-utils.cjs +43 -0
  37. package/dist/cjs/chrome/notifications-utils.native.js +46 -0
  38. package/dist/cjs/chrome/notifications-utils.native.js.map +1 -0
  39. package/dist/cjs/chrome/shortcuts.cjs +58 -0
  40. package/dist/cjs/chrome/shortcuts.native.js +72 -0
  41. package/dist/cjs/chrome/shortcuts.native.js.map +1 -0
  42. package/dist/cjs/chrome/user-session.cjs +100 -0
  43. package/dist/cjs/chrome/user-session.native.js +120 -0
  44. package/dist/cjs/chrome/user-session.native.js.map +1 -0
  45. package/dist/cjs/config/BackofficeProvider.cjs +164 -0
  46. package/dist/cjs/config/BackofficeProvider.native.js +217 -0
  47. package/dist/cjs/config/BackofficeProvider.native.js.map +1 -0
  48. package/dist/cjs/config/capabilities.cjs +123 -0
  49. package/dist/cjs/config/capabilities.native.js +155 -0
  50. package/dist/cjs/config/capabilities.native.js.map +1 -0
  51. package/dist/cjs/config/events.cjs +18 -0
  52. package/dist/cjs/config/events.native.js +21 -0
  53. package/dist/cjs/config/events.native.js.map +1 -0
  54. package/dist/cjs/config/slots.cjs +18 -0
  55. package/dist/cjs/config/slots.native.js +21 -0
  56. package/dist/cjs/config/slots.native.js.map +1 -0
  57. package/dist/cjs/i18n/en.cjs +101 -0
  58. package/dist/cjs/i18n/en.native.js +104 -0
  59. package/dist/cjs/i18n/en.native.js.map +1 -0
  60. package/dist/cjs/i18n/index.cjs +77 -0
  61. package/dist/cjs/i18n/index.native.js +102 -0
  62. package/dist/cjs/i18n/index.native.js.map +1 -0
  63. package/dist/cjs/index.cjs +109 -0
  64. package/dist/cjs/index.native.js +112 -0
  65. package/dist/cjs/index.native.js.map +1 -0
  66. package/dist/cjs/navigation/navigator.cjs +144 -0
  67. package/dist/cjs/navigation/navigator.native.js +307 -0
  68. package/dist/cjs/navigation/navigator.native.js.map +1 -0
  69. package/dist/cjs/navigation/paths.cjs +103 -0
  70. package/dist/cjs/navigation/paths.native.js +143 -0
  71. package/dist/cjs/navigation/paths.native.js.map +1 -0
  72. package/dist/cjs/oneAdapter/index.cjs +53 -0
  73. package/dist/cjs/oneAdapter/index.native.js +64 -0
  74. package/dist/cjs/oneAdapter/index.native.js.map +1 -0
  75. package/dist/cjs/shared/useFrappeApp.cjs +56 -0
  76. package/dist/cjs/shared/useFrappeApp.native.js +60 -0
  77. package/dist/cjs/shared/useFrappeApp.native.js.map +1 -0
  78. package/dist/cjs/shell/BackofficeShell.cjs +194 -0
  79. package/dist/cjs/shell/BackofficeShell.native.js +223 -0
  80. package/dist/cjs/shell/BackofficeShell.native.js.map +1 -0
  81. package/dist/cjs/shell/WorkspaceSidebar.cjs +115 -0
  82. package/dist/cjs/shell/WorkspaceSidebar.native.js +143 -0
  83. package/dist/cjs/shell/WorkspaceSidebar.native.js.map +1 -0
  84. package/dist/cjs/shell/useWorkspaces.cjs +131 -0
  85. package/dist/cjs/shell/useWorkspaces.native.js +173 -0
  86. package/dist/cjs/shell/useWorkspaces.native.js.map +1 -0
  87. package/dist/cjs/shell/workspaceIcons.cjs +64 -0
  88. package/dist/cjs/shell/workspaceIcons.native.js +67 -0
  89. package/dist/cjs/shell/workspaceIcons.native.js.map +1 -0
  90. package/dist/esm/chrome/BackofficeAwesomebar.mjs +70 -0
  91. package/dist/esm/chrome/BackofficeAwesomebar.mjs.map +1 -0
  92. package/dist/esm/chrome/BackofficeAwesomebar.native.js +78 -0
  93. package/dist/esm/chrome/BackofficeAwesomebar.native.js.map +1 -0
  94. package/dist/esm/chrome/CommandPalette.mjs +428 -0
  95. package/dist/esm/chrome/CommandPalette.mjs.map +1 -0
  96. package/dist/esm/chrome/CommandPalette.native.js +477 -0
  97. package/dist/esm/chrome/CommandPalette.native.js.map +1 -0
  98. package/dist/esm/chrome/HelpMenu.mjs +106 -0
  99. package/dist/esm/chrome/HelpMenu.mjs.map +1 -0
  100. package/dist/esm/chrome/HelpMenu.native.js +117 -0
  101. package/dist/esm/chrome/HelpMenu.native.js.map +1 -0
  102. package/dist/esm/chrome/NotificationsButton.mjs +87 -0
  103. package/dist/esm/chrome/NotificationsButton.mjs.map +1 -0
  104. package/dist/esm/chrome/NotificationsButton.native.js +87 -0
  105. package/dist/esm/chrome/NotificationsButton.native.js.map +1 -0
  106. package/dist/esm/chrome/UserMenu.mjs +133 -0
  107. package/dist/esm/chrome/UserMenu.mjs.map +1 -0
  108. package/dist/esm/chrome/UserMenu.native.js +147 -0
  109. package/dist/esm/chrome/UserMenu.native.js.map +1 -0
  110. package/dist/esm/chrome/awesomebar-data.mjs +144 -0
  111. package/dist/esm/chrome/awesomebar-data.mjs.map +1 -0
  112. package/dist/esm/chrome/awesomebar-data.native.js +190 -0
  113. package/dist/esm/chrome/awesomebar-data.native.js.map +1 -0
  114. package/dist/esm/chrome/command-palette-model.mjs +183 -0
  115. package/dist/esm/chrome/command-palette-model.mjs.map +1 -0
  116. package/dist/esm/chrome/command-palette-model.native.js +273 -0
  117. package/dist/esm/chrome/command-palette-model.native.js.map +1 -0
  118. package/dist/esm/chrome/help-about-dialog.mjs +111 -0
  119. package/dist/esm/chrome/help-about-dialog.mjs.map +1 -0
  120. package/dist/esm/chrome/help-about-dialog.native.js +118 -0
  121. package/dist/esm/chrome/help-about-dialog.native.js.map +1 -0
  122. package/dist/esm/chrome/help-shortcuts-dialog.mjs +130 -0
  123. package/dist/esm/chrome/help-shortcuts-dialog.mjs.map +1 -0
  124. package/dist/esm/chrome/help-shortcuts-dialog.native.js +140 -0
  125. package/dist/esm/chrome/help-shortcuts-dialog.native.js.map +1 -0
  126. package/dist/esm/chrome/notifications-data.mjs +165 -0
  127. package/dist/esm/chrome/notifications-data.mjs.map +1 -0
  128. package/dist/esm/chrome/notifications-data.native.js +232 -0
  129. package/dist/esm/chrome/notifications-data.native.js.map +1 -0
  130. package/dist/esm/chrome/notifications-panel.mjs +146 -0
  131. package/dist/esm/chrome/notifications-panel.mjs.map +1 -0
  132. package/dist/esm/chrome/notifications-panel.native.js +161 -0
  133. package/dist/esm/chrome/notifications-panel.native.js.map +1 -0
  134. package/dist/esm/chrome/notifications-utils.mjs +17 -0
  135. package/dist/esm/chrome/notifications-utils.mjs.map +1 -0
  136. package/dist/esm/chrome/notifications-utils.native.js +17 -0
  137. package/dist/esm/chrome/notifications-utils.native.js.map +1 -0
  138. package/dist/esm/chrome/shortcuts.mjs +33 -0
  139. package/dist/esm/chrome/shortcuts.mjs.map +1 -0
  140. package/dist/esm/chrome/shortcuts.native.js +44 -0
  141. package/dist/esm/chrome/shortcuts.native.js.map +1 -0
  142. package/dist/esm/chrome/user-session.mjs +73 -0
  143. package/dist/esm/chrome/user-session.mjs.map +1 -0
  144. package/dist/esm/chrome/user-session.native.js +79 -0
  145. package/dist/esm/chrome/user-session.native.js.map +1 -0
  146. package/dist/esm/config/BackofficeProvider.mjs +132 -0
  147. package/dist/esm/config/BackofficeProvider.mjs.map +1 -0
  148. package/dist/esm/config/BackofficeProvider.native.js +171 -0
  149. package/dist/esm/config/BackofficeProvider.native.js.map +1 -0
  150. package/dist/esm/config/capabilities.mjs +97 -0
  151. package/dist/esm/config/capabilities.mjs.map +1 -0
  152. package/dist/esm/config/capabilities.native.js +126 -0
  153. package/dist/esm/config/capabilities.native.js.map +1 -0
  154. package/dist/esm/config/events.mjs +2 -0
  155. package/dist/esm/config/events.mjs.map +1 -0
  156. package/dist/esm/config/events.native.js +2 -0
  157. package/dist/esm/config/events.native.js.map +1 -0
  158. package/dist/esm/config/slots.mjs +2 -0
  159. package/dist/esm/config/slots.mjs.map +1 -0
  160. package/dist/esm/config/slots.native.js +2 -0
  161. package/dist/esm/config/slots.native.js.map +1 -0
  162. package/dist/esm/i18n/en.mjs +76 -0
  163. package/dist/esm/i18n/en.mjs.map +1 -0
  164. package/dist/esm/i18n/en.native.js +76 -0
  165. package/dist/esm/i18n/en.native.js.map +1 -0
  166. package/dist/esm/i18n/index.mjs +40 -0
  167. package/dist/esm/i18n/index.mjs.map +1 -0
  168. package/dist/esm/i18n/index.native.js +62 -0
  169. package/dist/esm/i18n/index.native.js.map +1 -0
  170. package/dist/esm/index.mjs +26 -0
  171. package/dist/esm/index.mjs.map +1 -0
  172. package/dist/esm/index.native.js +26 -0
  173. package/dist/esm/index.native.js.map +1 -0
  174. package/dist/esm/navigation/navigator.mjs +115 -0
  175. package/dist/esm/navigation/navigator.mjs.map +1 -0
  176. package/dist/esm/navigation/navigator.native.js +275 -0
  177. package/dist/esm/navigation/navigator.native.js.map +1 -0
  178. package/dist/esm/navigation/paths.mjs +66 -0
  179. package/dist/esm/navigation/paths.mjs.map +1 -0
  180. package/dist/esm/navigation/paths.native.js +103 -0
  181. package/dist/esm/navigation/paths.native.js.map +1 -0
  182. package/dist/esm/oneAdapter/index.mjs +28 -0
  183. package/dist/esm/oneAdapter/index.mjs.map +1 -0
  184. package/dist/esm/oneAdapter/index.native.js +36 -0
  185. package/dist/esm/oneAdapter/index.native.js.map +1 -0
  186. package/dist/esm/shared/useFrappeApp.mjs +31 -0
  187. package/dist/esm/shared/useFrappeApp.mjs.map +1 -0
  188. package/dist/esm/shared/useFrappeApp.native.js +32 -0
  189. package/dist/esm/shared/useFrappeApp.native.js.map +1 -0
  190. package/dist/esm/shell/BackofficeShell.mjs +169 -0
  191. package/dist/esm/shell/BackofficeShell.mjs.map +1 -0
  192. package/dist/esm/shell/BackofficeShell.native.js +195 -0
  193. package/dist/esm/shell/BackofficeShell.native.js.map +1 -0
  194. package/dist/esm/shell/WorkspaceSidebar.mjs +89 -0
  195. package/dist/esm/shell/WorkspaceSidebar.mjs.map +1 -0
  196. package/dist/esm/shell/WorkspaceSidebar.native.js +114 -0
  197. package/dist/esm/shell/WorkspaceSidebar.native.js.map +1 -0
  198. package/dist/esm/shell/useWorkspaces.mjs +104 -0
  199. package/dist/esm/shell/useWorkspaces.mjs.map +1 -0
  200. package/dist/esm/shell/useWorkspaces.native.js +143 -0
  201. package/dist/esm/shell/useWorkspaces.native.js.map +1 -0
  202. package/dist/esm/shell/workspaceIcons.mjs +39 -0
  203. package/dist/esm/shell/workspaceIcons.mjs.map +1 -0
  204. package/dist/esm/shell/workspaceIcons.native.js +39 -0
  205. package/dist/esm/shell/workspaceIcons.native.js.map +1 -0
  206. package/dist/jsx/chrome/BackofficeAwesomebar.mjs +70 -0
  207. package/dist/jsx/chrome/BackofficeAwesomebar.mjs.map +1 -0
  208. package/dist/jsx/chrome/BackofficeAwesomebar.native.js +106 -0
  209. package/dist/jsx/chrome/BackofficeAwesomebar.native.js.map +1 -0
  210. package/dist/jsx/chrome/CommandPalette.mjs +428 -0
  211. package/dist/jsx/chrome/CommandPalette.mjs.map +1 -0
  212. package/dist/jsx/chrome/CommandPalette.native.js +505 -0
  213. package/dist/jsx/chrome/CommandPalette.native.js.map +1 -0
  214. package/dist/jsx/chrome/HelpMenu.mjs +106 -0
  215. package/dist/jsx/chrome/HelpMenu.mjs.map +1 -0
  216. package/dist/jsx/chrome/HelpMenu.native.js +145 -0
  217. package/dist/jsx/chrome/HelpMenu.native.js.map +1 -0
  218. package/dist/jsx/chrome/NotificationsButton.mjs +87 -0
  219. package/dist/jsx/chrome/NotificationsButton.mjs.map +1 -0
  220. package/dist/jsx/chrome/NotificationsButton.native.js +115 -0
  221. package/dist/jsx/chrome/NotificationsButton.native.js.map +1 -0
  222. package/dist/jsx/chrome/UserMenu.mjs +133 -0
  223. package/dist/jsx/chrome/UserMenu.mjs.map +1 -0
  224. package/dist/jsx/chrome/UserMenu.native.js +186 -0
  225. package/dist/jsx/chrome/UserMenu.native.js.map +1 -0
  226. package/dist/jsx/chrome/awesomebar-data.mjs +144 -0
  227. package/dist/jsx/chrome/awesomebar-data.mjs.map +1 -0
  228. package/dist/jsx/chrome/awesomebar-data.native.js +233 -0
  229. package/dist/jsx/chrome/awesomebar-data.native.js.map +1 -0
  230. package/dist/jsx/chrome/command-palette-model.mjs +183 -0
  231. package/dist/jsx/chrome/command-palette-model.mjs.map +1 -0
  232. package/dist/jsx/chrome/command-palette-model.native.js +301 -0
  233. package/dist/jsx/chrome/command-palette-model.native.js.map +1 -0
  234. package/dist/jsx/chrome/help-about-dialog.mjs +111 -0
  235. package/dist/jsx/chrome/help-about-dialog.mjs.map +1 -0
  236. package/dist/jsx/chrome/help-about-dialog.native.js +146 -0
  237. package/dist/jsx/chrome/help-about-dialog.native.js.map +1 -0
  238. package/dist/jsx/chrome/help-shortcuts-dialog.mjs +130 -0
  239. package/dist/jsx/chrome/help-shortcuts-dialog.mjs.map +1 -0
  240. package/dist/jsx/chrome/help-shortcuts-dialog.native.js +168 -0
  241. package/dist/jsx/chrome/help-shortcuts-dialog.native.js.map +1 -0
  242. package/dist/jsx/chrome/notifications-data.mjs +165 -0
  243. package/dist/jsx/chrome/notifications-data.mjs.map +1 -0
  244. package/dist/jsx/chrome/notifications-data.native.js +271 -0
  245. package/dist/jsx/chrome/notifications-data.native.js.map +1 -0
  246. package/dist/jsx/chrome/notifications-panel.mjs +146 -0
  247. package/dist/jsx/chrome/notifications-panel.mjs.map +1 -0
  248. package/dist/jsx/chrome/notifications-panel.native.js +189 -0
  249. package/dist/jsx/chrome/notifications-panel.native.js.map +1 -0
  250. package/dist/jsx/chrome/notifications-utils.mjs +17 -0
  251. package/dist/jsx/chrome/notifications-utils.mjs.map +1 -0
  252. package/dist/jsx/chrome/notifications-utils.native.js +46 -0
  253. package/dist/jsx/chrome/notifications-utils.native.js.map +1 -0
  254. package/dist/jsx/chrome/shortcuts.mjs +33 -0
  255. package/dist/jsx/chrome/shortcuts.mjs.map +1 -0
  256. package/dist/jsx/chrome/shortcuts.native.js +72 -0
  257. package/dist/jsx/chrome/shortcuts.native.js.map +1 -0
  258. package/dist/jsx/chrome/user-session.mjs +73 -0
  259. package/dist/jsx/chrome/user-session.mjs.map +1 -0
  260. package/dist/jsx/chrome/user-session.native.js +120 -0
  261. package/dist/jsx/chrome/user-session.native.js.map +1 -0
  262. package/dist/jsx/config/BackofficeProvider.mjs +132 -0
  263. package/dist/jsx/config/BackofficeProvider.mjs.map +1 -0
  264. package/dist/jsx/config/BackofficeProvider.native.js +217 -0
  265. package/dist/jsx/config/BackofficeProvider.native.js.map +1 -0
  266. package/dist/jsx/config/capabilities.mjs +97 -0
  267. package/dist/jsx/config/capabilities.mjs.map +1 -0
  268. package/dist/jsx/config/capabilities.native.js +155 -0
  269. package/dist/jsx/config/capabilities.native.js.map +1 -0
  270. package/dist/jsx/config/events.mjs +2 -0
  271. package/dist/jsx/config/events.mjs.map +1 -0
  272. package/dist/jsx/config/events.native.js +21 -0
  273. package/dist/jsx/config/events.native.js.map +1 -0
  274. package/dist/jsx/config/slots.mjs +2 -0
  275. package/dist/jsx/config/slots.mjs.map +1 -0
  276. package/dist/jsx/config/slots.native.js +21 -0
  277. package/dist/jsx/config/slots.native.js.map +1 -0
  278. package/dist/jsx/i18n/en.mjs +76 -0
  279. package/dist/jsx/i18n/en.mjs.map +1 -0
  280. package/dist/jsx/i18n/en.native.js +104 -0
  281. package/dist/jsx/i18n/en.native.js.map +1 -0
  282. package/dist/jsx/i18n/index.mjs +40 -0
  283. package/dist/jsx/i18n/index.mjs.map +1 -0
  284. package/dist/jsx/i18n/index.native.js +102 -0
  285. package/dist/jsx/i18n/index.native.js.map +1 -0
  286. package/dist/jsx/index.js +26 -0
  287. package/dist/jsx/index.js.map +1 -0
  288. package/dist/jsx/index.mjs +26 -0
  289. package/dist/jsx/index.mjs.map +1 -0
  290. package/dist/jsx/index.native.js +112 -0
  291. package/dist/jsx/index.native.js.map +1 -0
  292. package/dist/jsx/navigation/navigator.mjs +115 -0
  293. package/dist/jsx/navigation/navigator.mjs.map +1 -0
  294. package/dist/jsx/navigation/navigator.native.js +307 -0
  295. package/dist/jsx/navigation/navigator.native.js.map +1 -0
  296. package/dist/jsx/navigation/paths.mjs +66 -0
  297. package/dist/jsx/navigation/paths.mjs.map +1 -0
  298. package/dist/jsx/navigation/paths.native.js +143 -0
  299. package/dist/jsx/navigation/paths.native.js.map +1 -0
  300. package/dist/jsx/oneAdapter/index.mjs +28 -0
  301. package/dist/jsx/oneAdapter/index.mjs.map +1 -0
  302. package/dist/jsx/oneAdapter/index.native.js +64 -0
  303. package/dist/jsx/oneAdapter/index.native.js.map +1 -0
  304. package/dist/jsx/shared/useFrappeApp.mjs +31 -0
  305. package/dist/jsx/shared/useFrappeApp.mjs.map +1 -0
  306. package/dist/jsx/shared/useFrappeApp.native.js +60 -0
  307. package/dist/jsx/shared/useFrappeApp.native.js.map +1 -0
  308. package/dist/jsx/shell/BackofficeShell.mjs +169 -0
  309. package/dist/jsx/shell/BackofficeShell.mjs.map +1 -0
  310. package/dist/jsx/shell/BackofficeShell.native.js +223 -0
  311. package/dist/jsx/shell/BackofficeShell.native.js.map +1 -0
  312. package/dist/jsx/shell/WorkspaceSidebar.mjs +89 -0
  313. package/dist/jsx/shell/WorkspaceSidebar.mjs.map +1 -0
  314. package/dist/jsx/shell/WorkspaceSidebar.native.js +143 -0
  315. package/dist/jsx/shell/WorkspaceSidebar.native.js.map +1 -0
  316. package/dist/jsx/shell/useWorkspaces.mjs +104 -0
  317. package/dist/jsx/shell/useWorkspaces.mjs.map +1 -0
  318. package/dist/jsx/shell/useWorkspaces.native.js +173 -0
  319. package/dist/jsx/shell/useWorkspaces.native.js.map +1 -0
  320. package/dist/jsx/shell/workspaceIcons.mjs +39 -0
  321. package/dist/jsx/shell/workspaceIcons.mjs.map +1 -0
  322. package/dist/jsx/shell/workspaceIcons.native.js +67 -0
  323. package/dist/jsx/shell/workspaceIcons.native.js.map +1 -0
  324. package/package.json +123 -0
  325. package/src/chrome/BackofficeAwesomebar.tsx +89 -0
  326. package/src/chrome/CommandPalette.spec.tsx +252 -0
  327. package/src/chrome/CommandPalette.tsx +520 -0
  328. package/src/chrome/HelpMenu.tsx +119 -0
  329. package/src/chrome/NotificationsButton.tsx +87 -0
  330. package/src/chrome/UserMenu.tsx +173 -0
  331. package/src/chrome/awesomebar-data.tsx +252 -0
  332. package/src/chrome/command-palette-model.ts +291 -0
  333. package/src/chrome/help-about-dialog.tsx +130 -0
  334. package/src/chrome/help-shortcuts-dialog.tsx +144 -0
  335. package/src/chrome/notifications-data.tsx +287 -0
  336. package/src/chrome/notifications-panel.tsx +175 -0
  337. package/src/chrome/notifications-utils.tsx +44 -0
  338. package/src/chrome/shortcuts.ts +53 -0
  339. package/src/chrome/user-session.tsx +116 -0
  340. package/src/config/BackofficeProvider.spec.tsx +126 -0
  341. package/src/config/BackofficeProvider.tsx +315 -0
  342. package/src/config/capabilities.ts +172 -0
  343. package/src/config/events.ts +19 -0
  344. package/src/config/slots.ts +28 -0
  345. package/src/i18n/en.ts +83 -0
  346. package/src/i18n/index.ts +69 -0
  347. package/src/index.ts +150 -0
  348. package/src/navigation/navigator.spec.ts +106 -0
  349. package/src/navigation/navigator.ts +209 -0
  350. package/src/navigation/paths.ts +146 -0
  351. package/src/oneAdapter/index.tsx +61 -0
  352. package/src/shared/useFrappeApp.ts +39 -0
  353. package/src/shell/BackofficeShell.tsx +222 -0
  354. package/src/shell/WorkspaceSidebar.tsx +119 -0
  355. package/src/shell/useWorkspaces.ts +202 -0
  356. package/src/shell/workspaceIcons.ts +76 -0
  357. package/types/chrome/BackofficeAwesomebar.d.ts +12 -0
  358. package/types/chrome/BackofficeAwesomebar.d.ts.map +1 -0
  359. package/types/chrome/CommandPalette.d.ts +6 -0
  360. package/types/chrome/CommandPalette.d.ts.map +1 -0
  361. package/types/chrome/HelpMenu.d.ts +2 -0
  362. package/types/chrome/HelpMenu.d.ts.map +1 -0
  363. package/types/chrome/NotificationsButton.d.ts +2 -0
  364. package/types/chrome/NotificationsButton.d.ts.map +1 -0
  365. package/types/chrome/UserMenu.d.ts +6 -0
  366. package/types/chrome/UserMenu.d.ts.map +1 -0
  367. package/types/chrome/awesomebar-data.d.ts +52 -0
  368. package/types/chrome/awesomebar-data.d.ts.map +1 -0
  369. package/types/chrome/command-palette-model.d.ts +47 -0
  370. package/types/chrome/command-palette-model.d.ts.map +1 -0
  371. package/types/chrome/help-about-dialog.d.ts +12 -0
  372. package/types/chrome/help-about-dialog.d.ts.map +1 -0
  373. package/types/chrome/help-shortcuts-dialog.d.ts +6 -0
  374. package/types/chrome/help-shortcuts-dialog.d.ts.map +1 -0
  375. package/types/chrome/notifications-data.d.ts +34 -0
  376. package/types/chrome/notifications-data.d.ts.map +1 -0
  377. package/types/chrome/notifications-panel.d.ts +11 -0
  378. package/types/chrome/notifications-panel.d.ts.map +1 -0
  379. package/types/chrome/notifications-utils.d.ts +21 -0
  380. package/types/chrome/notifications-utils.d.ts.map +1 -0
  381. package/types/chrome/shortcuts.d.ts +6 -0
  382. package/types/chrome/shortcuts.d.ts.map +1 -0
  383. package/types/chrome/user-session.d.ts +19 -0
  384. package/types/chrome/user-session.d.ts.map +1 -0
  385. package/types/config/BackofficeProvider.d.ts +128 -0
  386. package/types/config/BackofficeProvider.d.ts.map +1 -0
  387. package/types/config/capabilities.d.ts +109 -0
  388. package/types/config/capabilities.d.ts.map +1 -0
  389. package/types/config/events.d.ts +39 -0
  390. package/types/config/events.d.ts.map +1 -0
  391. package/types/config/slots.d.ts +32 -0
  392. package/types/config/slots.d.ts.map +1 -0
  393. package/types/i18n/en.d.ts +83 -0
  394. package/types/i18n/en.d.ts.map +1 -0
  395. package/types/i18n/index.d.ts +27 -0
  396. package/types/i18n/index.d.ts.map +1 -0
  397. package/types/index.d.ts +55 -0
  398. package/types/index.d.ts.map +1 -0
  399. package/types/navigation/navigator.d.ts +98 -0
  400. package/types/navigation/navigator.d.ts.map +1 -0
  401. package/types/navigation/paths.d.ts +60 -0
  402. package/types/navigation/paths.d.ts.map +1 -0
  403. package/types/oneAdapter/index.d.ts +8 -0
  404. package/types/oneAdapter/index.d.ts.map +1 -0
  405. package/types/shared/useFrappeApp.d.ts +10 -0
  406. package/types/shared/useFrappeApp.d.ts.map +1 -0
  407. package/types/shell/BackofficeShell.d.ts +10 -0
  408. package/types/shell/BackofficeShell.d.ts.map +1 -0
  409. package/types/shell/WorkspaceSidebar.d.ts +9 -0
  410. package/types/shell/WorkspaceSidebar.d.ts.map +1 -0
  411. package/types/shell/useWorkspaces.d.ts +43 -0
  412. package/types/shell/useWorkspaces.d.ts.map +1 -0
  413. package/types/shell/workspaceIcons.d.ts +9 -0
  414. package/types/shell/workspaceIcons.d.ts.map +1 -0
@@ -0,0 +1,287 @@
1
+ import React from "react";
2
+ /**
3
+ * Live Notification Log feed for the navbar bell (desk parity).
4
+ *
5
+ * Data flows through the live client (`useFrappeCollection` + `useLiveQuery`)
6
+ * so realtime inserts pop into the panel without a refetch. Read state is a
7
+ * different story: the desk RPCs flip `read` with `update_modified=False`
8
+ * (no realtime doc_update event), so this hook layers local optimistic
9
+ * overrides on top of the collection and lets a later sync reconcile.
10
+ *
11
+ * RPC shapes (probed against the reference bench):
12
+ * - POST /api/method/frappe.desk.doctype.notification_log.notification_log.mark_as_read
13
+ * body {docname} → {} (sets read=1)
14
+ * - POST /api/method/frappe.desk.doctype.notification_log.notification_log.mark_all_as_read
15
+ * body {} → {} (marks every unread row for the session user)
16
+ * - Fallback per doc: POST frappe.client.set_value
17
+ * {doctype: "Notification Log", name, fieldname: "read", value: 1}
18
+ */
19
+ import {
20
+ Order,
21
+ useFrappeCollection,
22
+ useLiveQuery,
23
+ type FixtureProvider,
24
+ type FrappeApp,
25
+ } from "@multiplatform.one/frappe";
26
+ import { useFrappeConfig } from "@multiplatform.one/frappe-ui";
27
+ import { useCallback, useEffect, useMemo, useState } from "react";
28
+ import { useFrappeApp } from "../shared/useFrappeApp";
29
+ import { parseFrappeTimestamp, stripHtmlToText } from "./notifications-utils";
30
+
31
+ export interface NotificationLogDoc {
32
+ name: string;
33
+ doctype: string;
34
+ subject?: string;
35
+ type?: string;
36
+ document_type?: string;
37
+ document_name?: string;
38
+ read?: 0 | 1;
39
+ from_user?: string;
40
+ creation?: string;
41
+ }
42
+
43
+ /** A Notification Log row prepared for display. */
44
+ export interface NotificationItem extends NotificationLogDoc {
45
+ /** Server `read` merged with local optimistic mark-as-read overrides. */
46
+ unread: boolean;
47
+ /** Subject with HTML stripped ("Jane mentioned you in a comment: …"). */
48
+ subjectText: string;
49
+ /** `creation` as epoch ms (undefined when unparseable). */
50
+ timestampMs?: number;
51
+ }
52
+
53
+ const NOTIFICATION_FIELDS = [
54
+ "name",
55
+ "subject",
56
+ "type",
57
+ "document_type",
58
+ "document_name",
59
+ "read",
60
+ "from_user",
61
+ "creation",
62
+ ] as const;
63
+
64
+ /** Recent-notifications window (desk loads pages of 20; badge caps at 99+). */
65
+ const NOTIFICATION_WINDOW_LIMIT = 100;
66
+
67
+ const MARK_AS_READ_METHOD = "frappe.desk.doctype.notification_log.notification_log.mark_as_read";
68
+ const MARK_ALL_AS_READ_METHOD =
69
+ "frappe.desk.doctype.notification_log.notification_log.mark_all_as_read";
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Session user (module cache — one fetch per FrappeApp per session)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const sessionUserCache = new WeakMap<FrappeApp, Promise<string | undefined>>();
76
+
77
+ interface SessionUserState {
78
+ user?: string;
79
+ /** True once the probe settled (Guest / unreachable resolve to no user). */
80
+ resolved: boolean;
81
+ }
82
+
83
+ function useNotificationsSessionUser(fixtureMode: boolean): SessionUserState {
84
+ const app = useFrappeApp();
85
+ const [state, setState] = useState<SessionUserState>({ resolved: false });
86
+
87
+ useEffect(() => {
88
+ // Fixture mode has no session endpoint; report resolved-with-no-user.
89
+ // No app (no provider config) settles the same way so the panel never
90
+ // skeletons forever.
91
+ if (fixtureMode || !app) {
92
+ setState({ resolved: true });
93
+ return;
94
+ }
95
+ let cancelled = false;
96
+ let promise = sessionUserCache.get(app);
97
+ if (!promise) {
98
+ promise = app
99
+ .call()
100
+ .get<{ message?: string }>("frappe.auth.get_logged_user")
101
+ .then((res) => {
102
+ const user = res.payload?.message;
103
+ return user && user !== "Guest" ? user : undefined;
104
+ })
105
+ .catch(() => {
106
+ // Evict failed probes so a later mount can retry.
107
+ sessionUserCache.delete(app);
108
+ return undefined;
109
+ });
110
+ sessionUserCache.set(app, promise);
111
+ }
112
+ promise.then((user) => {
113
+ if (!cancelled) setState({ user, resolved: true });
114
+ });
115
+ return () => {
116
+ cancelled = true;
117
+ };
118
+ }, [app, fixtureMode]);
119
+
120
+ return state;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Mark-as-read RPCs
125
+ // ---------------------------------------------------------------------------
126
+
127
+ async function setReadViaSetValue(app: FrappeApp, name: string): Promise<void> {
128
+ await app.call().post("frappe.client.set_value", {
129
+ doctype: "Notification Log",
130
+ name,
131
+ fieldname: "read",
132
+ value: 1,
133
+ });
134
+ }
135
+
136
+ async function markAsReadOnServer(app: FrappeApp, name: string): Promise<void> {
137
+ try {
138
+ await app.call().post(MARK_AS_READ_METHOD, { docname: name });
139
+ } catch {
140
+ // Desk RPC unavailable (older bench / stripped app) — plain doc write.
141
+ await setReadViaSetValue(app, name);
142
+ }
143
+ }
144
+
145
+ async function markAllAsReadOnServer(
146
+ app: FrappeApp,
147
+ unreadNames: readonly string[],
148
+ ): Promise<void> {
149
+ try {
150
+ await app.call().post(MARK_ALL_AS_READ_METHOD, {});
151
+ } catch {
152
+ await Promise.all(unreadNames.map((name) => setReadViaSetValue(app, name)));
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Hook
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export interface UseNotificationsResult {
161
+ /** Prepared rows, newest first. */
162
+ items: NotificationItem[];
163
+ /** Unread rows within the window (badge value). */
164
+ unreadCount: number;
165
+ /** True until the session probe + first collection snapshot settle. */
166
+ isLoading: boolean;
167
+ /** Mark one notification read (optimistic; reverts if the RPC fails). */
168
+ markAsRead: (name: string) => Promise<void>;
169
+ /** Mark every unread notification read (optimistic; reverts on failure). */
170
+ markAllAsRead: () => Promise<void>;
171
+ }
172
+
173
+ export function useNotifications(): UseNotificationsResult {
174
+ const config = useFrappeConfig();
175
+ const app = useFrappeApp();
176
+ const fixtureMode = Boolean(config?.fixtures);
177
+ const { user: sessionUser, resolved: sessionResolved } = useNotificationsSessionUser(fixtureMode);
178
+
179
+ // Stable connection config (fresh identities would churn the collection).
180
+ const connectionConfig = useMemo(
181
+ () =>
182
+ config
183
+ ? {
184
+ baseURL: config.baseURL,
185
+ socketPort: config.socketPort,
186
+ auth: config.auth,
187
+ syncOptions: config.syncOptions,
188
+ socketOptions: config.socketOptions,
189
+ fixtures: config.fixtures as FixtureProvider | undefined,
190
+ }
191
+ : undefined,
192
+ [config],
193
+ );
194
+
195
+ // Live mode waits for the session user (the for_user filter needs it);
196
+ // fixture mode reads the fixture set unfiltered.
197
+ const ready = Boolean(connectionConfig) && (fixtureMode || Boolean(sessionUser));
198
+ const collection = useFrappeCollection<NotificationLogDoc>(
199
+ ready ? connectionConfig : undefined,
200
+ "Notification Log",
201
+ {
202
+ fields: NOTIFICATION_FIELDS,
203
+ filters: !fixtureMode && sessionUser ? [["for_user", "=", sessionUser]] : undefined,
204
+ orderBy: { field: "creation", order: Order.Desc },
205
+ limit: NOTIFICATION_WINDOW_LIMIT,
206
+ },
207
+ );
208
+
209
+ const { data: rawData, isLoading: queryLoading } = useLiveQuery(
210
+ (q) => (collection ? q.from({ _notification: collection }) : null),
211
+ [collection],
212
+ );
213
+
214
+ // Local read overrides — see module note on why realtime can't carry these.
215
+ const [readOverrides, setReadOverrides] = useState<ReadonlySet<string>>(new Set());
216
+
217
+ const items = useMemo<NotificationItem[]>(() => {
218
+ const docs = (rawData as NotificationLogDoc[] | undefined) ?? [];
219
+ return docs
220
+ .filter((doc) => doc.name)
221
+ .map((doc) => ({
222
+ ...doc,
223
+ unread: !doc.read && !readOverrides.has(doc.name),
224
+ subjectText: stripHtmlToText(doc.subject),
225
+ timestampMs: parseFrappeTimestamp(doc.creation),
226
+ }))
227
+ .sort((a, b) => (b.timestampMs ?? 0) - (a.timestampMs ?? 0) || a.name.localeCompare(b.name));
228
+ }, [rawData, readOverrides]);
229
+
230
+ const unreadCount = useMemo(() => items.filter((item) => item.unread).length, [items]);
231
+
232
+ const isLoading =
233
+ (Boolean(connectionConfig) && !fixtureMode && !sessionResolved) ||
234
+ // Session resolved without a user (Guest / unreachable bench): settle on
235
+ // an empty feed instead of skeleting forever.
236
+ (ready && (collection === undefined || Boolean(queryLoading)));
237
+
238
+ const addOverrides = useCallback((names: readonly string[]) => {
239
+ if (!names.length) return;
240
+ setReadOverrides((prev) => {
241
+ const next = new Set(prev);
242
+ for (const name of names) next.add(name);
243
+ return next;
244
+ });
245
+ }, []);
246
+
247
+ const removeOverrides = useCallback((names: readonly string[]) => {
248
+ if (!names.length) return;
249
+ setReadOverrides((prev) => {
250
+ const next = new Set(prev);
251
+ for (const name of names) next.delete(name);
252
+ return next;
253
+ });
254
+ }, []);
255
+
256
+ const markAsRead = useCallback(
257
+ async (name: string) => {
258
+ addOverrides([name]);
259
+ if (fixtureMode || !app) return;
260
+ try {
261
+ await markAsReadOnServer(app, name);
262
+ } catch (err) {
263
+ removeOverrides([name]);
264
+ throw err;
265
+ }
266
+ },
267
+ [app, fixtureMode, addOverrides, removeOverrides],
268
+ );
269
+
270
+ const markAllAsRead = useCallback(async () => {
271
+ const unreadNames = items.filter((item) => item.unread).map((item) => item.name);
272
+ if (!unreadNames.length) return;
273
+ addOverrides(unreadNames);
274
+ if (fixtureMode || !app) return;
275
+ try {
276
+ await markAllAsReadOnServer(app, unreadNames);
277
+ } catch (err) {
278
+ removeOverrides(unreadNames);
279
+ throw err;
280
+ }
281
+ }, [app, fixtureMode, items, addOverrides, removeOverrides]);
282
+
283
+ return useMemo(
284
+ () => ({ items, unreadCount, isLoading, markAsRead, markAllAsRead }),
285
+ [items, unreadCount, isLoading, markAsRead, markAllAsRead],
286
+ );
287
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Notifications panel body (desk bell-dropdown parity): header with
3
+ * "Notifications" + mark-all-as-read + settings placeholder, then the
4
+ * newest-first Notification Log list. Presentational — data, routing, and
5
+ * the popover/sheet shell live in NotificationsButton.
6
+ */
7
+ import {
8
+ Avatar,
9
+ Button,
10
+ EmptyState,
11
+ Separator,
12
+ SizableText,
13
+ Skeleton,
14
+ SkeletonCircle,
15
+ Text,
16
+ View,
17
+ XStack,
18
+ YStack,
19
+ } from "@multiplatform.one/components";
20
+ import { formatRelativeTimestamp } from "@multiplatform.one/theme";
21
+ import { GearSixIcon } from "@phosphor-icons/react";
22
+ import { useTranslation } from "react-i18next";
23
+ import { BACKOFFICE_I18N_NAMESPACE } from "../i18n";
24
+ import type { NotificationItem } from "./notifications-data";
25
+
26
+ export interface NotificationsPanelProps {
27
+ items: readonly NotificationItem[];
28
+ isLoading: boolean;
29
+ /** Panel width — a fixed measure when floating, "100%" in sheet mode. */
30
+ width?: number | string;
31
+ onItemPress: (item: NotificationItem) => void;
32
+ onMarkAllRead: () => void;
33
+ }
34
+
35
+ function NotificationRow({ item, onPress }: { item: NotificationItem; onPress: () => void }) {
36
+ const { t } = useTranslation(BACKOFFICE_I18N_NAMESPACE);
37
+ const subject = item.subjectText || t("notifications.untitled", "Notification");
38
+ const relative = formatRelativeTimestamp(item.timestampMs ?? item.creation);
39
+ return (
40
+ <XStack
41
+ alignItems="flex-start"
42
+ gap="$3"
43
+ paddingHorizontal="$3"
44
+ paddingVertical="$2.5"
45
+ backgroundColor={item.unread ? "$color2" : "transparent"}
46
+ hoverStyle={{ backgroundColor: "$color3" }}
47
+ pressStyle={{ backgroundColor: "$color4" }}
48
+ cursor="pointer"
49
+ onPress={onPress}
50
+ role="button"
51
+ aria-label={subject}
52
+ data-testid="backoffice-notification-row"
53
+ >
54
+ <Avatar name={item.from_user || "?"} size="$2.5" />
55
+ <YStack flex={1} minWidth={0} gap="$1">
56
+ <Text
57
+ fontSize="$3"
58
+ fontWeight={item.unread ? "600" : "400"}
59
+ color={item.unread ? "$color12" : "$color11"}
60
+ numberOfLines={2}
61
+ >
62
+ {subject}
63
+ </Text>
64
+ {relative ? (
65
+ <Text fontSize="$2" color="$color9">
66
+ {relative}
67
+ </Text>
68
+ ) : null}
69
+ </YStack>
70
+ {item.unread && (
71
+ <View
72
+ width={8}
73
+ height={8}
74
+ borderRadius={1000}
75
+ backgroundColor="$accentBackground"
76
+ marginTop="$1.5"
77
+ flexShrink={0}
78
+ aria-hidden
79
+ />
80
+ )}
81
+ </XStack>
82
+ );
83
+ }
84
+
85
+ function NotificationSkeletonRows() {
86
+ return (
87
+ <YStack data-testid="backoffice-notifications-loading">
88
+ {Array.from({ length: 3 }, (_, index) => (
89
+ <XStack
90
+ key={index}
91
+ alignItems="center"
92
+ gap="$3"
93
+ paddingHorizontal="$3"
94
+ paddingVertical="$2.5"
95
+ >
96
+ <SkeletonCircle size={28} />
97
+ <YStack flex={1} gap="$1.5">
98
+ <Skeleton height={12} width="85%" />
99
+ <Skeleton height={10} width={64} />
100
+ </YStack>
101
+ </XStack>
102
+ ))}
103
+ </YStack>
104
+ );
105
+ }
106
+
107
+ export function NotificationsPanel({
108
+ items,
109
+ isLoading,
110
+ width,
111
+ onItemPress,
112
+ onMarkAllRead,
113
+ }: NotificationsPanelProps) {
114
+ const { t } = useTranslation(BACKOFFICE_I18N_NAMESPACE);
115
+ const hasUnread = items.some((item) => item.unread);
116
+
117
+ return (
118
+ <YStack
119
+ width={width}
120
+ maxWidth="100%"
121
+ paddingVertical="$2"
122
+ data-testid="backoffice-notifications-panel"
123
+ >
124
+ <XStack alignItems="center" gap="$2" paddingHorizontal="$3" paddingBottom="$2">
125
+ <SizableText size="$4" fontWeight="700" flex={1} numberOfLines={1}>
126
+ {t("notifications.title", "Notifications")}
127
+ </SizableText>
128
+ <Button
129
+ chromeless
130
+ size="$2"
131
+ disabled={!hasUnread}
132
+ opacity={hasUnread ? 1 : 0.5}
133
+ onPress={onMarkAllRead}
134
+ data-testid="backoffice-notifications-mark-all"
135
+ >
136
+ {t("notifications.markAllRead", "Mark all as read")}
137
+ </Button>
138
+ {/* Desk links to /app/notification-settings; the backoffice has no
139
+ settings surface yet — disabled placeholder documents the gap. */}
140
+ <Button
141
+ chromeless
142
+ circular
143
+ size="$2"
144
+ icon={GearSixIcon}
145
+ disabled
146
+ opacity={0.5}
147
+ aria-label={t(
148
+ "notifications.settingsUnavailable",
149
+ "Notification settings (not available yet)",
150
+ )}
151
+ />
152
+ </XStack>
153
+ <Separator />
154
+ {isLoading ? (
155
+ <NotificationSkeletonRows />
156
+ ) : items.length === 0 ? (
157
+ <EmptyState
158
+ compact
159
+ paddingVertical="$6"
160
+ title={t("notifications.emptyTitle", "No new notifications")}
161
+ description={t(
162
+ "notifications.emptyDescription",
163
+ "Mentions, assignments, and shares will show up here.",
164
+ )}
165
+ />
166
+ ) : (
167
+ <YStack>
168
+ {items.map((item) => (
169
+ <NotificationRow key={item.name} item={item} onPress={() => onItemPress(item)} />
170
+ ))}
171
+ </YStack>
172
+ )}
173
+ </YStack>
174
+ );
175
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Tiny pure helpers for the notifications panel. Notification Log subjects
3
+ * arrive as HTML fragments ('<b>Jane</b> mentioned you in <a>…</a>') and
4
+ * timestamps in frappe's "YYYY-MM-DD HH:mm:ss.ffffff" register.
5
+ */
6
+
7
+ /**
8
+ * Strip HTML tags from a Notification Log subject and return readable text.
9
+ * Closing block tags / <br> become spaces so "<div>a</div><div>b</div>"
10
+ * reads "a b"; basic entities are decoded (&amp; last so "&amp;lt;" stays
11
+ * literal "&lt;"); whitespace collapses to single spaces.
12
+ */
13
+ export function stripHtmlToText(html: string | null | undefined): string {
14
+ if (!html) return "";
15
+ const withBreaks = html.replace(/<\s*(?:br|\/p|\/div|\/li|\/tr|\/h[1-6])\s*\/?\s*>/gi, " ");
16
+ const withoutTags = withBreaks.replace(/<[^>]*>/g, "");
17
+ const decoded = withoutTags
18
+ .replace(/&nbsp;/gi, " ")
19
+ .replace(/&lt;/gi, "<")
20
+ .replace(/&gt;/gi, ">")
21
+ .replace(/&quot;/gi, '"')
22
+ .replace(/&#39;|&apos;/gi, "'")
23
+ .replace(/&amp;/gi, "&");
24
+ return decoded.replace(/\s+/g, " ").trim();
25
+ }
26
+
27
+ /**
28
+ * Parse a frappe timestamp ("2026-08-12 09:34:09.025850") to epoch ms.
29
+ * The space separator and microsecond precision trip Safari/JSC, so
30
+ * normalize to ISO-ish ("T" separator, milliseconds) before Date parsing.
31
+ * Frappe emits server-local wall time with no zone; parsing as local time
32
+ * matches how desk renders it. Unparseable input → undefined.
33
+ */
34
+ export function parseFrappeTimestamp(value: string | null | undefined): number | undefined {
35
+ if (!value) return undefined;
36
+ const isoish = value
37
+ .trim()
38
+ .replace(" ", "T")
39
+ .replace(/(\.\d{3})\d+$/, "$1");
40
+ const parsed = new Date(isoish).getTime();
41
+ if (!Number.isNaN(parsed)) return parsed;
42
+ const fallback = new Date(value).getTime();
43
+ return Number.isNaN(fallback) ? undefined : fallback;
44
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Global backoffice keyboard shortcuts (web only).
3
+ *
4
+ * INTEGRATION: the shell (user-owned) should call this once near the root of
5
+ * the desk chrome and wire `onFocusSearch` to the awesomebar, e.g.:
6
+ *
7
+ * useBackofficeShortcuts({ onFocusSearch: () => focusAwesomebar() });
8
+ *
9
+ * Bindings (keep chrome/help-shortcuts-dialog.tsx in sync):
10
+ * - Mod+K / Mod+G → onFocusSearch (opens the command palette)
11
+ *
12
+ * SSR / native / GJS safe: no-ops when there is no DOM document. Events from
13
+ * editable targets (inputs, textareas, selects, contenteditable) are ignored
14
+ * so typing is never hijacked. Cleans up its listener on unmount.
15
+ */
16
+ import { useEffect, useRef } from "react";
17
+
18
+ export interface BackofficeShortcutHandlers {
19
+ /** Focus the awesomebar / global search (Mod+K, Mod+G). */
20
+ onFocusSearch?: () => void;
21
+ }
22
+
23
+ function isEditableTarget(target: EventTarget | null): boolean {
24
+ if (typeof HTMLElement === "undefined" || !(target instanceof HTMLElement)) return false;
25
+ if (target.isContentEditable) return true;
26
+ const tag = target.tagName;
27
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
28
+ }
29
+
30
+ export function useBackofficeShortcuts(handlers: BackofficeShortcutHandlers): void {
31
+ // Ref keeps the listener stable while consumers pass inline handler objects.
32
+ const handlersRef = useRef(handlers);
33
+ handlersRef.current = handlers;
34
+
35
+ useEffect(() => {
36
+ if (typeof document === "undefined" || typeof document.addEventListener !== "function") {
37
+ return;
38
+ }
39
+ const onKeyDown = (event: KeyboardEvent) => {
40
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
41
+ if (event.repeat || isEditableTarget(event.target)) return;
42
+ const key = event.key.toLowerCase();
43
+ if (key === "k" || key === "g") {
44
+ const { onFocusSearch } = handlersRef.current;
45
+ if (!onFocusSearch) return;
46
+ event.preventDefault();
47
+ onFocusSearch();
48
+ }
49
+ };
50
+ document.addEventListener("keydown", onKeyDown);
51
+ return () => document.removeEventListener("keydown", onKeyDown);
52
+ }, []);
53
+ }
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ /**
3
+ * Session-user info for the navbar user menu, module-cached so the avatar
4
+ * button and the menu header fetch once per session (per FrappeApp).
5
+ *
6
+ * Flow (one cached promise): frappe.auth.get_logged_user →
7
+ * frappe.client.get_value(User, <name>, ["full_name","user_image","email"]).
8
+ */
9
+ import type { FrappeApp } from "@multiplatform.one/frappe";
10
+ import { useFrappeConfig } from "@multiplatform.one/frappe-ui";
11
+ import { useEffect, useMemo, useState } from "react";
12
+ import { useFrappeApp } from "../shared/useFrappeApp";
13
+
14
+ export interface SessionUserInfo {
15
+ /** Session user docname (e.g. "Administrator" or an email login). */
16
+ user: string;
17
+ fullName: string;
18
+ email?: string;
19
+ /** Absolute avatar URL (User.user_image resolved against the bench). */
20
+ imageUrl?: string;
21
+ }
22
+
23
+ interface UserDocBits {
24
+ full_name?: string;
25
+ user_image?: string | null;
26
+ email?: string;
27
+ }
28
+
29
+ // Keyed by FrappeApp instance: a different bench/auth config gets its own
30
+ // entry (useFrappeApp memoizes apps per config). Failed fetches self-evict
31
+ // so the next mount retries.
32
+ let sessionCache = new WeakMap<FrappeApp, Promise<SessionUserInfo>>();
33
+
34
+ /** Logout hygiene: drops every cached session-user fetch in this module. */
35
+ export function clearSessionUserCache(): void {
36
+ sessionCache = new WeakMap();
37
+ }
38
+
39
+ /** Resolve a User.user_image value (usually "/files/…") to an absolute URL. */
40
+ export function resolveUserImageUrl(
41
+ userImage: string | null | undefined,
42
+ baseURL: string,
43
+ ): string | undefined {
44
+ if (!userImage) return undefined;
45
+ // Already absolute (http(s), data:, blob:, …) — leave untouched.
46
+ if (/^[a-z][a-z0-9+.-]*:/i.test(userImage)) return userImage;
47
+ return `${baseURL.replace(/\/+$/, "")}${userImage.startsWith("/") ? "" : "/"}${userImage}`;
48
+ }
49
+
50
+ async function fetchSessionUserInfo(app: FrappeApp, baseURL: string): Promise<SessionUserInfo> {
51
+ const user = await app.auth().getLoggedInUser();
52
+ if (!user) throw new Error("No session user");
53
+ // Param format probed against the bench: filters takes the plain docname,
54
+ // fieldname a JSON array (the http client JSON-encodes non-string params).
55
+ const res = await app.call().get<{ message?: UserDocBits }>("frappe.client.get_value", {
56
+ doctype: "User",
57
+ filters: user,
58
+ fieldname: ["full_name", "user_image", "email"],
59
+ });
60
+ const bits = res.payload?.message ?? {};
61
+ return {
62
+ user,
63
+ fullName: bits.full_name || user,
64
+ email: bits.email || (user.includes("@") ? user : undefined),
65
+ imageUrl: resolveUserImageUrl(bits.user_image, baseURL),
66
+ };
67
+ }
68
+
69
+ /** Module-cached session-user info from the ambient Frappe connection. */
70
+ export function useSessionUserInfo(): {
71
+ info?: SessionUserInfo;
72
+ loading: boolean;
73
+ error?: Error;
74
+ } {
75
+ const app = useFrappeApp();
76
+ const config = useFrappeConfig();
77
+ const baseURL = config?.baseURL ?? "";
78
+ const [info, setInfo] = useState<SessionUserInfo | undefined>(undefined);
79
+ const [loading, setLoading] = useState(true);
80
+ const [error, setError] = useState<Error | undefined>(undefined);
81
+
82
+ useEffect(() => {
83
+ if (!app) {
84
+ setInfo(undefined);
85
+ setLoading(false);
86
+ return;
87
+ }
88
+ let cancelled = false;
89
+ let promise = sessionCache.get(app);
90
+ if (!promise) {
91
+ promise = fetchSessionUserInfo(app, baseURL);
92
+ sessionCache.set(app, promise);
93
+ promise.catch(() => {
94
+ if (sessionCache.get(app) === promise) sessionCache.delete(app);
95
+ });
96
+ }
97
+ setLoading(true);
98
+ setError(undefined);
99
+ promise
100
+ .then((value) => {
101
+ if (cancelled) return;
102
+ setInfo(value);
103
+ setLoading(false);
104
+ })
105
+ .catch((err: Error) => {
106
+ if (cancelled) return;
107
+ setError(err);
108
+ setLoading(false);
109
+ });
110
+ return () => {
111
+ cancelled = true;
112
+ };
113
+ }, [app, baseURL]);
114
+
115
+ return useMemo(() => ({ info, loading, error }), [info, loading, error]);
116
+ }