@maxal_studio/kratosjs-react 1.0.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 (529) hide show
  1. package/README.md +44 -0
  2. package/dist/FieldRenderer.d.ts +13 -0
  3. package/dist/FieldRenderer.js +62 -0
  4. package/dist/FormRenderer.d.ts +7 -0
  5. package/dist/FormRenderer.js +78 -0
  6. package/dist/TableRenderer.d.ts +2 -0
  7. package/dist/TableRenderer.js +1 -0
  8. package/dist/api/actionsApi.d.ts +23 -0
  9. package/dist/api/actionsApi.js +46 -0
  10. package/dist/api/authenticatedFetch.d.ts +8 -0
  11. package/dist/api/authenticatedFetch.js +31 -0
  12. package/dist/api/exportApi.d.ts +18 -0
  13. package/dist/api/exportApi.js +50 -0
  14. package/dist/api/http.d.ts +24 -0
  15. package/dist/api/http.js +52 -0
  16. package/dist/api/resourceApi.d.ts +37 -0
  17. package/dist/api/resourceApi.js +52 -0
  18. package/dist/api/tableApi.d.ts +83 -0
  19. package/dist/api/tableApi.js +51 -0
  20. package/dist/api/urls.d.ts +19 -0
  21. package/dist/api/urls.js +46 -0
  22. package/dist/app.d.ts +101 -0
  23. package/dist/app.js +89 -0
  24. package/dist/auth/AuthContext.d.ts +22 -0
  25. package/dist/auth/AuthContext.js +147 -0
  26. package/dist/auth/LoginPage.d.ts +10 -0
  27. package/dist/auth/LoginPage.js +179 -0
  28. package/dist/auth/ProtectedRoute.d.ts +12 -0
  29. package/dist/auth/ProtectedRoute.js +22 -0
  30. package/dist/auth/authApiClient.d.ts +24 -0
  31. package/dist/auth/authApiClient.js +95 -0
  32. package/dist/auth/types.d.ts +103 -0
  33. package/dist/auth/types.js +1 -0
  34. package/dist/components/ActionFormModal.d.ts +22 -0
  35. package/dist/components/ActionFormModal.js +8 -0
  36. package/dist/components/AdminPanel.d.ts +11 -0
  37. package/dist/components/AdminPanel.js +194 -0
  38. package/dist/components/Checkbox.d.ts +10 -0
  39. package/dist/components/Checkbox.js +8 -0
  40. package/dist/components/CheckboxField.d.ts +7 -0
  41. package/dist/components/CheckboxField.js +26 -0
  42. package/dist/components/ColorPickerField.d.ts +7 -0
  43. package/dist/components/ColorPickerField.js +26 -0
  44. package/dist/components/DateTimePickerField.d.ts +7 -0
  45. package/dist/components/DateTimePickerField.js +64 -0
  46. package/dist/components/FileUploadField.d.ts +9 -0
  47. package/dist/components/FileUploadField.js +478 -0
  48. package/dist/components/GlobalSearch.d.ts +22 -0
  49. package/dist/components/GlobalSearch.js +181 -0
  50. package/dist/components/GroupField.d.ts +7 -0
  51. package/dist/components/GroupField.js +23 -0
  52. package/dist/components/HiddenField.d.ts +3 -0
  53. package/dist/components/HiddenField.js +10 -0
  54. package/dist/components/ModalBreadcrumb.d.ts +5 -0
  55. package/dist/components/ModalBreadcrumb.js +33 -0
  56. package/dist/components/ModalDrawer.d.ts +15 -0
  57. package/dist/components/ModalDrawer.js +40 -0
  58. package/dist/components/RadioField.d.ts +7 -0
  59. package/dist/components/RadioField.js +26 -0
  60. package/dist/components/RepeaterField.d.ts +3 -0
  61. package/dist/components/RepeaterField.js +191 -0
  62. package/dist/components/ResourceModalRenderer.d.ts +10 -0
  63. package/dist/components/ResourceModalRenderer.js +80 -0
  64. package/dist/components/RichEditorField.d.ts +3 -0
  65. package/dist/components/RichEditorField.js +655 -0
  66. package/dist/components/SectionField.d.ts +9 -0
  67. package/dist/components/SectionField.js +111 -0
  68. package/dist/components/SelectField.d.ts +8 -0
  69. package/dist/components/SelectField.js +523 -0
  70. package/dist/components/TabsField.d.ts +10 -0
  71. package/dist/components/TabsField.js +214 -0
  72. package/dist/components/TagsInputField.d.ts +7 -0
  73. package/dist/components/TagsInputField.js +172 -0
  74. package/dist/components/TextInputField.d.ts +7 -0
  75. package/dist/components/TextInputField.js +44 -0
  76. package/dist/components/TextareaField.d.ts +7 -0
  77. package/dist/components/TextareaField.js +31 -0
  78. package/dist/components/ToggleField.d.ts +7 -0
  79. package/dist/components/ToggleField.js +57 -0
  80. package/dist/components/ViewModal.d.ts +25 -0
  81. package/dist/components/ViewModal.js +159 -0
  82. package/dist/components/blocks/BlockRenderer.d.ts +7 -0
  83. package/dist/components/blocks/BlockRenderer.js +36 -0
  84. package/dist/components/blocks/FormBlockRenderer.d.ts +6 -0
  85. package/dist/components/blocks/FormBlockRenderer.js +110 -0
  86. package/dist/components/blocks/TableBlockRenderer.d.ts +6 -0
  87. package/dist/components/blocks/TableBlockRenderer.js +12 -0
  88. package/dist/components/blocks/TabsBlockRenderer.d.ts +7 -0
  89. package/dist/components/blocks/TabsBlockRenderer.js +11 -0
  90. package/dist/components/blocks/WidgetBlockRenderer.d.ts +6 -0
  91. package/dist/components/blocks/WidgetBlockRenderer.js +11 -0
  92. package/dist/components/columns/CheckboxColumnComponent.d.ts +6 -0
  93. package/dist/components/columns/CheckboxColumnComponent.js +21 -0
  94. package/dist/components/columns/ColorColumnComponent.d.ts +3 -0
  95. package/dist/components/columns/ColorColumnComponent.js +11 -0
  96. package/dist/components/columns/DeeplinkWrapper.d.ts +15 -0
  97. package/dist/components/columns/DeeplinkWrapper.js +85 -0
  98. package/dist/components/columns/IconColumnComponent.d.ts +3 -0
  99. package/dist/components/columns/IconColumnComponent.js +52 -0
  100. package/dist/components/columns/ImageColumnComponent.d.ts +3 -0
  101. package/dist/components/columns/ImageColumnComponent.js +98 -0
  102. package/dist/components/columns/MediaColumnComponent.d.ts +3 -0
  103. package/dist/components/columns/MediaColumnComponent.js +160 -0
  104. package/dist/components/columns/SelectColumnComponent.d.ts +6 -0
  105. package/dist/components/columns/SelectColumnComponent.js +26 -0
  106. package/dist/components/columns/TagsColumnComponent.d.ts +3 -0
  107. package/dist/components/columns/TagsColumnComponent.js +18 -0
  108. package/dist/components/columns/TextColumnComponent.d.ts +11 -0
  109. package/dist/components/columns/TextColumnComponent.js +107 -0
  110. package/dist/components/columns/TextInputColumnComponent.d.ts +6 -0
  111. package/dist/components/columns/TextInputColumnComponent.js +18 -0
  112. package/dist/components/columns/ToggleColumnComponent.d.ts +6 -0
  113. package/dist/components/columns/ToggleColumnComponent.js +25 -0
  114. package/dist/components/columns/VideoColumnComponent.d.ts +3 -0
  115. package/dist/components/columns/VideoColumnComponent.js +125 -0
  116. package/dist/components/columns/ViewColumnComponent.d.ts +3 -0
  117. package/dist/components/columns/ViewColumnComponent.js +7 -0
  118. package/dist/components/errors/ErrorBoundary.d.ts +23 -0
  119. package/dist/components/errors/ErrorBoundary.js +33 -0
  120. package/dist/components/filters/CustomFilterComponent.d.ts +10 -0
  121. package/dist/components/filters/CustomFilterComponent.js +33 -0
  122. package/dist/components/filters/DateFilterComponent.d.ts +15 -0
  123. package/dist/components/filters/DateFilterComponent.js +132 -0
  124. package/dist/components/filters/QueryBuilderFilterComponent.d.ts +11 -0
  125. package/dist/components/filters/QueryBuilderFilterComponent.js +200 -0
  126. package/dist/components/layout/Header.d.ts +10 -0
  127. package/dist/components/layout/Header.js +70 -0
  128. package/dist/components/layout/PanelBrandMark.d.ts +8 -0
  129. package/dist/components/layout/PanelBrandMark.js +28 -0
  130. package/dist/components/layout/Sidebar.d.ts +35 -0
  131. package/dist/components/layout/Sidebar.js +125 -0
  132. package/dist/components/modals/RelationCreateModal.d.ts +19 -0
  133. package/dist/components/modals/RelationCreateModal.js +57 -0
  134. package/dist/components/modals/ResourceFormModal.d.ts +37 -0
  135. package/dist/components/modals/ResourceFormModal.js +44 -0
  136. package/dist/components/modals/useResourceForm.d.ts +40 -0
  137. package/dist/components/modals/useResourceForm.js +138 -0
  138. package/dist/components/modals/view/RecordActions.d.ts +17 -0
  139. package/dist/components/modals/view/RecordActions.js +16 -0
  140. package/dist/components/modals/view/RecordDetails.d.ts +13 -0
  141. package/dist/components/modals/view/RecordDetails.js +29 -0
  142. package/dist/components/modals/view/RelationPanel.d.ts +18 -0
  143. package/dist/components/modals/view/RelationPanel.js +16 -0
  144. package/dist/components/modals/view/RelationTabs.d.ts +32 -0
  145. package/dist/components/modals/view/RelationTabs.js +42 -0
  146. package/dist/components/modals/view/useRecordView.d.ts +18 -0
  147. package/dist/components/modals/view/useRecordView.js +114 -0
  148. package/dist/components/pages/PageRenderer.d.ts +6 -0
  149. package/dist/components/pages/PageRenderer.js +107 -0
  150. package/dist/components/table/ColumnTogglePopup.d.ts +11 -0
  151. package/dist/components/table/ColumnTogglePopup.js +16 -0
  152. package/dist/components/table/GridCard.d.ts +21 -0
  153. package/dist/components/table/GridCard.js +30 -0
  154. package/dist/components/table/GridView.d.ts +23 -0
  155. package/dist/components/table/GridView.js +49 -0
  156. package/dist/components/table/LayoutToggle.d.ts +7 -0
  157. package/dist/components/table/LayoutToggle.js +9 -0
  158. package/dist/components/table/TableActionsDropdown.d.ts +13 -0
  159. package/dist/components/table/TableActionsDropdown.js +46 -0
  160. package/dist/components/table/TableBulkActions.d.ts +11 -0
  161. package/dist/components/table/TableBulkActions.js +21 -0
  162. package/dist/components/table/TableHeader.d.ts +14 -0
  163. package/dist/components/table/TableHeader.js +23 -0
  164. package/dist/components/table/TablePagination.d.ts +13 -0
  165. package/dist/components/table/TablePagination.js +55 -0
  166. package/dist/components/table/TableRow.d.ts +21 -0
  167. package/dist/components/table/TableRow.js +32 -0
  168. package/dist/components/table/TableSearchBar.d.ts +11 -0
  169. package/dist/components/table/TableSearchBar.js +12 -0
  170. package/dist/components/table/TableTabs.d.ts +14 -0
  171. package/dist/components/table/TableTabs.js +8 -0
  172. package/dist/components/ui/Badge.d.ts +6 -0
  173. package/dist/components/ui/Badge.js +12 -0
  174. package/dist/components/ui/Button.d.ts +22 -0
  175. package/dist/components/ui/Button.js +22 -0
  176. package/dist/components/ui/Card.d.ts +7 -0
  177. package/dist/components/ui/Card.js +5 -0
  178. package/dist/components/ui/ConfirmDialog.d.ts +19 -0
  179. package/dist/components/ui/ConfirmDialog.js +45 -0
  180. package/dist/components/ui/EmptyState.d.ts +9 -0
  181. package/dist/components/ui/EmptyState.js +6 -0
  182. package/dist/components/ui/ErrorAlert.d.ts +7 -0
  183. package/dist/components/ui/ErrorAlert.js +9 -0
  184. package/dist/components/ui/Input.d.ts +11 -0
  185. package/dist/components/ui/Input.js +10 -0
  186. package/dist/components/ui/Label.d.ts +5 -0
  187. package/dist/components/ui/Label.js +5 -0
  188. package/dist/components/ui/PillButton.d.ts +14 -0
  189. package/dist/components/ui/PillButton.js +19 -0
  190. package/dist/components/ui/Select.d.ts +7 -0
  191. package/dist/components/ui/Select.js +7 -0
  192. package/dist/components/ui/Spinner.d.ts +8 -0
  193. package/dist/components/ui/Spinner.js +14 -0
  194. package/dist/components/ui/Toast.d.ts +21 -0
  195. package/dist/components/ui/Toast.js +47 -0
  196. package/dist/components/ui/index.d.ts +24 -0
  197. package/dist/components/ui/index.js +12 -0
  198. package/dist/components/utils/HintDisplay.d.ts +11 -0
  199. package/dist/components/utils/HintDisplay.js +12 -0
  200. package/dist/components/utils/Icon.d.ts +22 -0
  201. package/dist/components/utils/Icon.js +22 -0
  202. package/dist/components/utils/MediaPreviewModal.d.ts +14 -0
  203. package/dist/components/utils/MediaPreviewModal.js +32 -0
  204. package/dist/components/utils/ViewFieldWrapper.d.ts +11 -0
  205. package/dist/components/utils/ViewFieldWrapper.js +9 -0
  206. package/dist/components/utils/layoutHelpers.d.ts +19 -0
  207. package/dist/components/utils/layoutHelpers.js +257 -0
  208. package/dist/components/widgets/ChartWidget.d.ts +16 -0
  209. package/dist/components/widgets/ChartWidget.js +192 -0
  210. package/dist/components/widgets/StatsWidget.d.ts +16 -0
  211. package/dist/components/widgets/StatsWidget.js +39 -0
  212. package/dist/components/widgets/WidgetRenderer.d.ts +10 -0
  213. package/dist/components/widgets/WidgetRenderer.js +50 -0
  214. package/dist/components/widgets/WidgetShell.d.ts +9 -0
  215. package/dist/components/widgets/WidgetShell.js +7 -0
  216. package/dist/contexts/AuthChallengeRegistryContext.d.ts +15 -0
  217. package/dist/contexts/AuthChallengeRegistryContext.js +15 -0
  218. package/dist/contexts/BlockRegistryContext.d.ts +18 -0
  219. package/dist/contexts/BlockRegistryContext.js +8 -0
  220. package/dist/contexts/ColumnRegistryContext.d.ts +8 -0
  221. package/dist/contexts/ColumnRegistryContext.js +30 -0
  222. package/dist/contexts/FieldRegistryContext.d.ts +13 -0
  223. package/dist/contexts/FieldRegistryContext.js +46 -0
  224. package/dist/contexts/PanelMetadataContext.d.ts +26 -0
  225. package/dist/contexts/PanelMetadataContext.js +26 -0
  226. package/dist/contexts/PanelProviders.d.ts +27 -0
  227. package/dist/contexts/PanelProviders.js +24 -0
  228. package/dist/contexts/ResourceModalContext.d.ts +26 -0
  229. package/dist/contexts/ResourceModalContext.js +76 -0
  230. package/dist/contexts/SlotRegistryContext.d.ts +19 -0
  231. package/dist/contexts/SlotRegistryContext.js +24 -0
  232. package/dist/contexts/TableRefreshContext.d.ts +10 -0
  233. package/dist/contexts/TableRefreshContext.js +30 -0
  234. package/dist/contexts/WidgetRegistryContext.d.ts +17 -0
  235. package/dist/contexts/WidgetRegistryContext.js +14 -0
  236. package/dist/contexts/createRegistryContext.d.ts +19 -0
  237. package/dist/contexts/createRegistryContext.js +20 -0
  238. package/dist/hooks/useAfterStateUpdated.d.ts +6 -0
  239. package/dist/hooks/useAfterStateUpdated.js +62 -0
  240. package/dist/hooks/useValidation.d.ts +26 -0
  241. package/dist/hooks/useValidation.js +76 -0
  242. package/dist/i18n/I18nProvider.d.ts +27 -0
  243. package/dist/i18n/I18nProvider.js +101 -0
  244. package/dist/i18n/LocaleSwitcher.d.ts +10 -0
  245. package/dist/i18n/LocaleSwitcher.js +30 -0
  246. package/dist/i18n/activeLocale.d.ts +11 -0
  247. package/dist/i18n/activeLocale.js +34 -0
  248. package/dist/i18n/buildClientI18n.d.ts +28 -0
  249. package/dist/i18n/buildClientI18n.js +67 -0
  250. package/dist/i18n/index.d.ts +11 -0
  251. package/dist/i18n/index.js +9 -0
  252. package/dist/i18n/locales/core/en.d.ts +225 -0
  253. package/dist/i18n/locales/core/en.js +252 -0
  254. package/dist/i18n/locales/core/index.d.ts +2 -0
  255. package/dist/i18n/locales/core/index.js +4 -0
  256. package/dist/i18n/locales/core/sq.d.ts +253 -0
  257. package/dist/i18n/locales/core/sq.js +255 -0
  258. package/dist/i18n/useFormatter.d.ts +18 -0
  259. package/dist/i18n/useFormatter.js +37 -0
  260. package/dist/i18n/useLocale.d.ts +11 -0
  261. package/dist/i18n/useLocale.js +11 -0
  262. package/dist/i18n/useTranslation.d.ts +12 -0
  263. package/dist/i18n/useTranslation.js +12 -0
  264. package/dist/index.d.ts +106 -0
  265. package/dist/index.js +101 -0
  266. package/dist/pages/ResourceListPage.d.ts +8 -0
  267. package/dist/pages/ResourceListPage.js +139 -0
  268. package/dist/plugin.d.ts +79 -0
  269. package/dist/plugin.js +34 -0
  270. package/dist/runtime/conditions.d.ts +35 -0
  271. package/dist/runtime/conditions.js +97 -0
  272. package/dist/runtime/formTraversal.d.ts +25 -0
  273. package/dist/runtime/formTraversal.js +37 -0
  274. package/dist/runtime/serializedFunctions.d.ts +41 -0
  275. package/dist/runtime/serializedFunctions.js +264 -0
  276. package/dist/slots/Slot.d.ts +24 -0
  277. package/dist/slots/Slot.js +29 -0
  278. package/dist/slots/SlotCluster.d.ts +22 -0
  279. package/dist/slots/SlotCluster.js +49 -0
  280. package/dist/slots/index.d.ts +7 -0
  281. package/dist/slots/index.js +4 -0
  282. package/dist/slots/mergeSlots.d.ts +18 -0
  283. package/dist/slots/mergeSlots.js +35 -0
  284. package/dist/slots/types.d.ts +87 -0
  285. package/dist/slots/types.js +30 -0
  286. package/dist/styles.css +1 -0
  287. package/dist/table/TableContext.d.ts +36 -0
  288. package/dist/table/TableContext.js +13 -0
  289. package/dist/table/TableRenderer.d.ts +29 -0
  290. package/dist/table/TableRenderer.js +159 -0
  291. package/dist/table/components/FiltersPanel.d.ts +11 -0
  292. package/dist/table/components/FiltersPanel.js +52 -0
  293. package/dist/table/components/TableToolbar.d.ts +28 -0
  294. package/dist/table/components/TableToolbar.js +27 -0
  295. package/dist/table/components/TableToolbarButton.d.ts +6 -0
  296. package/dist/table/components/TableToolbarButton.js +9 -0
  297. package/dist/table/components/TableView.d.ts +12 -0
  298. package/dist/table/components/TableView.js +21 -0
  299. package/dist/table/defaultRowActions.d.ts +21 -0
  300. package/dist/table/defaultRowActions.js +37 -0
  301. package/dist/table/hooks/useColumnVisibility.d.ts +13 -0
  302. package/dist/table/hooks/useColumnVisibility.js +59 -0
  303. package/dist/table/hooks/useEditableRows.d.ts +22 -0
  304. package/dist/table/hooks/useEditableRows.js +63 -0
  305. package/dist/table/hooks/useTableActions.d.ts +54 -0
  306. package/dist/table/hooks/useTableActions.js +313 -0
  307. package/dist/table/hooks/useTableData.d.ts +28 -0
  308. package/dist/table/hooks/useTableData.js +63 -0
  309. package/dist/table/hooks/useTableLayout.d.ts +12 -0
  310. package/dist/table/hooks/useTableLayout.js +31 -0
  311. package/dist/table/hooks/useTableQuery.d.ts +29 -0
  312. package/dist/table/hooks/useTableQuery.js +135 -0
  313. package/dist/types/index.d.ts +224 -0
  314. package/dist/types/index.js +6 -0
  315. package/dist/utils/classNames.d.ts +7 -0
  316. package/dist/utils/classNames.js +9 -0
  317. package/dist/utils/columnMediaDimensions.d.ts +13 -0
  318. package/dist/utils/columnMediaDimensions.js +29 -0
  319. package/dist/utils/columnVisibilityStorage.d.ts +22 -0
  320. package/dist/utils/columnVisibilityStorage.js +56 -0
  321. package/dist/utils/fieldErrors.d.ts +13 -0
  322. package/dist/utils/fieldErrors.js +25 -0
  323. package/dist/utils/formatValue.d.ts +28 -0
  324. package/dist/utils/formatValue.js +109 -0
  325. package/dist/utils/layoutStorage.d.ts +23 -0
  326. package/dist/utils/layoutStorage.js +53 -0
  327. package/dist/utils/redirectHandler.d.ts +7 -0
  328. package/dist/utils/redirectHandler.js +25 -0
  329. package/dist/utils/tableFormatters.d.ts +14 -0
  330. package/dist/utils/tableFormatters.js +93 -0
  331. package/dist/utils/widgetVisibilityStorage.d.ts +11 -0
  332. package/dist/utils/widgetVisibilityStorage.js +39 -0
  333. package/package.json +101 -0
  334. package/src/FieldRenderer.test.tsx +44 -0
  335. package/src/FieldRenderer.tsx +104 -0
  336. package/src/FormRenderer.containers.test.tsx +121 -0
  337. package/src/FormRenderer.test.tsx +174 -0
  338. package/src/FormRenderer.tsx +140 -0
  339. package/src/TableRenderer.tsx +2 -0
  340. package/src/api/actionsApi.ts +76 -0
  341. package/src/api/authenticatedFetch.ts +40 -0
  342. package/src/api/exportApi.ts +66 -0
  343. package/src/api/http.test.ts +58 -0
  344. package/src/api/http.ts +68 -0
  345. package/src/api/resourceApi.ts +88 -0
  346. package/src/api/tableApi.test.ts +108 -0
  347. package/src/api/tableApi.ts +107 -0
  348. package/src/api/urls.ts +50 -0
  349. package/src/app.test.tsx +67 -0
  350. package/src/app.tsx +181 -0
  351. package/src/auth/AuthContext.tsx +188 -0
  352. package/src/auth/LoginPage.tsx +380 -0
  353. package/src/auth/ProtectedRoute.tsx +39 -0
  354. package/src/auth/authApiClient.ts +109 -0
  355. package/src/auth/authFlow.test.tsx +168 -0
  356. package/src/auth/types.ts +104 -0
  357. package/src/components/ActionFormModal.tsx +45 -0
  358. package/src/components/AdminPanel.tsx +368 -0
  359. package/src/components/Checkbox.tsx +59 -0
  360. package/src/components/CheckboxField.tsx +88 -0
  361. package/src/components/ColorPickerField.tsx +93 -0
  362. package/src/components/DateTimePickerField.tsx +112 -0
  363. package/src/components/FileUploadField.tsx +841 -0
  364. package/src/components/GlobalSearch.tsx +436 -0
  365. package/src/components/GroupField.tsx +85 -0
  366. package/src/components/HiddenField.tsx +14 -0
  367. package/src/components/ModalBreadcrumb.tsx +74 -0
  368. package/src/components/ModalDrawer.tsx +137 -0
  369. package/src/components/RadioField.tsx +80 -0
  370. package/src/components/RepeaterField.tsx +546 -0
  371. package/src/components/ResourceModalRenderer.tsx +144 -0
  372. package/src/components/RichEditorField.tsx +942 -0
  373. package/src/components/SectionField.tsx +242 -0
  374. package/src/components/SelectField.tsx +843 -0
  375. package/src/components/TabsField.test.tsx +151 -0
  376. package/src/components/TabsField.tsx +386 -0
  377. package/src/components/TagsInputField.tsx +411 -0
  378. package/src/components/TextInputField.tsx +91 -0
  379. package/src/components/TextareaField.tsx +110 -0
  380. package/src/components/ToggleField.tsx +126 -0
  381. package/src/components/ViewModal.tsx +353 -0
  382. package/src/components/blocks/BlockRenderer.tsx +56 -0
  383. package/src/components/blocks/FormBlockRenderer.tsx +160 -0
  384. package/src/components/blocks/TableBlockRenderer.tsx +33 -0
  385. package/src/components/blocks/TabsBlockRenderer.tsx +49 -0
  386. package/src/components/blocks/WidgetBlockRenderer.tsx +19 -0
  387. package/src/components/columns/CheckboxColumnComponent.tsx +38 -0
  388. package/src/components/columns/ColorColumnComponent.tsx +23 -0
  389. package/src/components/columns/CustomColumn.test.tsx +55 -0
  390. package/src/components/columns/DeeplinkWrapper.tsx +103 -0
  391. package/src/components/columns/IconColumnComponent.tsx +55 -0
  392. package/src/components/columns/ImageColumnComponent.tsx +220 -0
  393. package/src/components/columns/MediaColumnComponent.tsx +294 -0
  394. package/src/components/columns/SelectColumnComponent.tsx +49 -0
  395. package/src/components/columns/TagsColumnComponent.tsx +46 -0
  396. package/src/components/columns/TextColumnComponent.tsx +191 -0
  397. package/src/components/columns/TextInputColumnComponent.tsx +35 -0
  398. package/src/components/columns/ToggleColumnComponent.tsx +56 -0
  399. package/src/components/columns/VideoColumnComponent.tsx +236 -0
  400. package/src/components/columns/ViewColumnComponent.tsx +9 -0
  401. package/src/components/errors/ErrorBoundary.tsx +58 -0
  402. package/src/components/filters/CustomFilterComponent.tsx +130 -0
  403. package/src/components/filters/DateFilterComponent.tsx +272 -0
  404. package/src/components/filters/QueryBuilderFilterComponent.tsx +502 -0
  405. package/src/components/layout/Header.tsx +212 -0
  406. package/src/components/layout/PanelBrandMark.tsx +61 -0
  407. package/src/components/layout/Sidebar.tsx +283 -0
  408. package/src/components/modals/RelationCreateModal.tsx +107 -0
  409. package/src/components/modals/ResourceFormModal.test.tsx +119 -0
  410. package/src/components/modals/ResourceFormModal.tsx +128 -0
  411. package/src/components/modals/useResourceForm.ts +207 -0
  412. package/src/components/modals/view/RecordActions.tsx +69 -0
  413. package/src/components/modals/view/RecordDetails.tsx +60 -0
  414. package/src/components/modals/view/RelationPanel.tsx +76 -0
  415. package/src/components/modals/view/RelationTabs.tsx +145 -0
  416. package/src/components/modals/view/useRecordView.ts +134 -0
  417. package/src/components/pages/PageRenderer.tsx +173 -0
  418. package/src/components/table/ColumnTogglePopup.tsx +85 -0
  419. package/src/components/table/GridCard.tsx +155 -0
  420. package/src/components/table/GridView.tsx +138 -0
  421. package/src/components/table/LayoutToggle.tsx +24 -0
  422. package/src/components/table/TableActionsDropdown.tsx +114 -0
  423. package/src/components/table/TableBulkActions.tsx +65 -0
  424. package/src/components/table/TableHeader.tsx +96 -0
  425. package/src/components/table/TablePagination.tsx +169 -0
  426. package/src/components/table/TableRow.tsx +155 -0
  427. package/src/components/table/TableSearchBar.tsx +66 -0
  428. package/src/components/table/TableTabs.tsx +49 -0
  429. package/src/components/ui/Badge.tsx +30 -0
  430. package/src/components/ui/Button.test.tsx +78 -0
  431. package/src/components/ui/Button.tsx +102 -0
  432. package/src/components/ui/Card.tsx +23 -0
  433. package/src/components/ui/ConfirmDialog.tsx +112 -0
  434. package/src/components/ui/EmptyState.tsx +24 -0
  435. package/src/components/ui/ErrorAlert.tsx +37 -0
  436. package/src/components/ui/Input.tsx +48 -0
  437. package/src/components/ui/Label.tsx +15 -0
  438. package/src/components/ui/PillButton.tsx +72 -0
  439. package/src/components/ui/Select.tsx +33 -0
  440. package/src/components/ui/Spinner.tsx +39 -0
  441. package/src/components/ui/Toast.tsx +105 -0
  442. package/src/components/ui/index.ts +24 -0
  443. package/src/components/utils/HintDisplay.tsx +26 -0
  444. package/src/components/utils/Icon.tsx +36 -0
  445. package/src/components/utils/MediaPreviewModal.tsx +114 -0
  446. package/src/components/utils/ViewFieldWrapper.tsx +23 -0
  447. package/src/components/utils/layoutHelpers.ts +267 -0
  448. package/src/components/widgets/ChartWidget.tsx +247 -0
  449. package/src/components/widgets/StatsWidget.tsx +72 -0
  450. package/src/components/widgets/WidgetRenderer.tsx +108 -0
  451. package/src/components/widgets/WidgetShell.tsx +37 -0
  452. package/src/contexts/AuthChallengeRegistryContext.tsx +29 -0
  453. package/src/contexts/BlockRegistryContext.tsx +28 -0
  454. package/src/contexts/ColumnRegistryContext.tsx +38 -0
  455. package/src/contexts/FieldRegistryContext.tsx +56 -0
  456. package/src/contexts/PanelMetadataContext.tsx +60 -0
  457. package/src/contexts/PanelProviders.tsx +85 -0
  458. package/src/contexts/ResourceModalContext.tsx +137 -0
  459. package/src/contexts/SlotRegistryContext.tsx +35 -0
  460. package/src/contexts/TableRefreshContext.tsx +44 -0
  461. package/src/contexts/WidgetRegistryContext.tsx +34 -0
  462. package/src/contexts/createRegistryContext.tsx +29 -0
  463. package/src/hooks/useAfterStateUpdated.ts +70 -0
  464. package/src/hooks/useValidation.test.ts +59 -0
  465. package/src/hooks/useValidation.ts +95 -0
  466. package/src/i18n/I18nProvider.tsx +128 -0
  467. package/src/i18n/LocaleSwitcher.tsx +50 -0
  468. package/src/i18n/activeLocale.ts +39 -0
  469. package/src/i18n/buildClientI18n.ts +101 -0
  470. package/src/i18n/i18n.test.tsx +140 -0
  471. package/src/i18n/index.ts +12 -0
  472. package/src/i18n/locales/core/en.ts +274 -0
  473. package/src/i18n/locales/core/index.ts +5 -0
  474. package/src/i18n/locales/core/sq.ts +275 -0
  475. package/src/i18n/useFormatter.ts +42 -0
  476. package/src/i18n/useLocale.ts +16 -0
  477. package/src/i18n/useTranslation.ts +17 -0
  478. package/src/index.ts +244 -0
  479. package/src/pages/ResourceListPage.tsx +205 -0
  480. package/src/plugin.ts +110 -0
  481. package/src/runtime/conditions.test.ts +99 -0
  482. package/src/runtime/conditions.ts +148 -0
  483. package/src/runtime/formTraversal.ts +41 -0
  484. package/src/runtime/serializedFunctions.test.ts +59 -0
  485. package/src/runtime/serializedFunctions.ts +284 -0
  486. package/src/slots/Slot.test.tsx +89 -0
  487. package/src/slots/Slot.tsx +47 -0
  488. package/src/slots/SlotCluster.test.tsx +95 -0
  489. package/src/slots/SlotCluster.tsx +107 -0
  490. package/src/slots/index.ts +15 -0
  491. package/src/slots/mergeSlots.test.ts +71 -0
  492. package/src/slots/mergeSlots.ts +40 -0
  493. package/src/slots/slotNames.test.ts +21 -0
  494. package/src/slots/types.ts +119 -0
  495. package/src/styles.css +437 -0
  496. package/src/table/TableContext.tsx +41 -0
  497. package/src/table/TableRenderer.test.tsx +197 -0
  498. package/src/table/TableRenderer.tsx +390 -0
  499. package/src/table/components/FiltersPanel.tsx +193 -0
  500. package/src/table/components/TableToolbar.tsx +153 -0
  501. package/src/table/components/TableToolbarButton.tsx +14 -0
  502. package/src/table/components/TableView.tsx +106 -0
  503. package/src/table/defaultRowActions.ts +43 -0
  504. package/src/table/hooks/useColumnVisibility.test.ts +51 -0
  505. package/src/table/hooks/useColumnVisibility.ts +71 -0
  506. package/src/table/hooks/useEditableRows.test.ts +69 -0
  507. package/src/table/hooks/useEditableRows.ts +89 -0
  508. package/src/table/hooks/useTableActions.ts +393 -0
  509. package/src/table/hooks/useTableData.ts +89 -0
  510. package/src/table/hooks/useTableLayout.ts +45 -0
  511. package/src/table/hooks/useTableQuery.test.ts +116 -0
  512. package/src/table/hooks/useTableQuery.ts +172 -0
  513. package/src/test/mockFetch.ts +67 -0
  514. package/src/test/setup.ts +25 -0
  515. package/src/types/index.ts +228 -0
  516. package/src/utils/classNames.ts +10 -0
  517. package/src/utils/columnMediaDimensions.ts +45 -0
  518. package/src/utils/columnVisibilityStorage.ts +55 -0
  519. package/src/utils/fieldErrors.test.ts +35 -0
  520. package/src/utils/fieldErrors.ts +27 -0
  521. package/src/utils/formatValue.test.tsx +65 -0
  522. package/src/utils/formatValue.tsx +117 -0
  523. package/src/utils/layoutStorage.ts +52 -0
  524. package/src/utils/redirectHandler.ts +29 -0
  525. package/src/utils/tableFormatters.test.ts +54 -0
  526. package/src/utils/tableFormatters.ts +104 -0
  527. package/src/utils/widgetVisibilityStorage.ts +38 -0
  528. package/tailwind.config.js +9 -0
  529. package/vite.config.ts +17 -0
@@ -0,0 +1,70 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useWatch, useFormContext } from 'react-hook-form';
3
+ import { executeSerializedFunction } from '../runtime/serializedFunctions';
4
+
5
+ /**
6
+ * Hook to execute afterStateUpdated callback when a field's value changes
7
+ * @param fieldName - Name of the field to watch
8
+ * @param callbackString - Serialized callback function as string
9
+ */
10
+ export function useAfterStateUpdated(fieldName: string, callbackString?: string) {
11
+ const { control, getValues, setValue } = useFormContext();
12
+ const currentValue = useWatch({ control, name: fieldName });
13
+ const previousValueRef = useRef<any>(currentValue);
14
+
15
+ useEffect(() => {
16
+ // Skip if no callback is provided
17
+ if (!callbackString) return;
18
+
19
+ // Skip on initial mount (when previousValue is undefined)
20
+ if (previousValueRef.current === undefined && currentValue === undefined) {
21
+ return;
22
+ }
23
+
24
+ // Check if value actually changed
25
+ const hasChanged = previousValueRef.current !== currentValue;
26
+
27
+ if (hasChanged) {
28
+ try {
29
+ // Create get function to access form values
30
+ const get = (field: string) => {
31
+ // Support dot notation for nested fields
32
+ const keys = field.split('.');
33
+ let value = getValues();
34
+
35
+ for (const key of keys) {
36
+ if (value === undefined || value === null) {
37
+ return undefined;
38
+ }
39
+ value = value[key];
40
+ }
41
+
42
+ return value;
43
+ };
44
+
45
+ // Create set function to update form values
46
+ const set = (field: string, value: any) => {
47
+ setValue(field, value, {
48
+ shouldValidate: true,
49
+ shouldDirty: true,
50
+ shouldTouch: true,
51
+ });
52
+ };
53
+
54
+ // Evaluate the callback function using the shared utility
55
+ // The function should be in the format: (get, set, state, old) => { ... }
56
+ // Reconstruct the function first, then call it with the parameters
57
+ const callback = executeSerializedFunction(callbackString);
58
+ if (typeof callback === 'function') {
59
+ // Call it with the required parameters
60
+ callback(get, set, currentValue, previousValueRef.current);
61
+ }
62
+ } catch (error) {
63
+ console.error('Error executing afterStateUpdated callback:', error);
64
+ }
65
+ }
66
+
67
+ // Update previous value for next comparison
68
+ previousValueRef.current = currentValue;
69
+ }, [currentValue, callbackString, getValues, setValue]);
70
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import React from 'react';
3
+ import { renderHook } from '@testing-library/react';
4
+ import { FormProvider, useForm } from 'react-hook-form';
5
+ import { useValidation } from './useValidation';
6
+
7
+ // The hook no longer re-implements rule semantics: it delegates to the shared
8
+ // ValidationEngine via a single RHF `validate` function. These tests confirm
9
+ // the wiring (required → native RHF, everything else → engine) and that the
10
+ // verdicts match the engine the backend also uses.
11
+
12
+ function wrapper({ children }: { children: React.ReactNode }) {
13
+ function Inner() {
14
+ const methods = useForm();
15
+ return React.createElement(FormProvider, methods as any, children);
16
+ }
17
+ return React.createElement(Inner);
18
+ }
19
+
20
+ function runValidate(result: ReturnType<typeof useValidation>, value: any, formValues: Record<string, any> = {}) {
21
+ return result.validate?.kratos(value, formValues);
22
+ }
23
+
24
+ describe('useValidation', () => {
25
+ it('maps required to RHF native (drives the asterisk)', () => {
26
+ const { result } = renderHook(() => useValidation(['required'], 'create', 'name'), { wrapper });
27
+ expect(result.current.required).toBe('This field is required');
28
+ });
29
+
30
+ it('delegates email validation to the engine', () => {
31
+ const { result } = renderHook(() => useValidation(['email'], 'create', 'email'), { wrapper });
32
+ expect(runValidate(result.current, 'a@b.com')).toBe(true);
33
+ expect(runValidate(result.current, 'nope')).toMatch(/valid email/);
34
+ });
35
+
36
+ it('delegates min length to the engine', () => {
37
+ const { result } = renderHook(() => useValidation(['min:3'], 'create', 'title'), { wrapper });
38
+ expect(runValidate(result.current, 'abc')).toBe(true);
39
+ expect(runValidate(result.current, 'ab')).toMatch(/at least 3 characters/);
40
+ });
41
+
42
+ it('enforces alpha (parity with the backend)', () => {
43
+ const { result } = renderHook(() => useValidation(['alpha'], 'create', 'code'), { wrapper });
44
+ expect(runValidate(result.current, 'abc')).toBe(true);
45
+ expect(runValidate(result.current, 'ab1')).toMatch(/letters/);
46
+ });
47
+
48
+ it('resolves cross-field rules against form values', () => {
49
+ const { result } = renderHook(() => useValidation(['same:password'], 'create', 'confirm'), { wrapper });
50
+ expect(runValidate(result.current, 'x', { password: 'x' })).toBe(true);
51
+ expect(runValidate(result.current, 'x', { password: 'y' })).toMatch(/must match password/);
52
+ });
53
+
54
+ it('skips rules whose condition is false', () => {
55
+ const rules = [{ rule: 'required', condition: false }];
56
+ const { result } = renderHook(() => useValidation(rules, 'create', 'name'), { wrapper });
57
+ expect(result.current.required).toBeUndefined();
58
+ });
59
+ });
@@ -0,0 +1,95 @@
1
+ import { useMemo } from 'react';
2
+ import { useWatch, useFormContext } from 'react-hook-form';
3
+ import { RHFValidationRules } from '../types';
4
+ import type { ValidationRule } from '@maxal_studio/kratosjs';
5
+ // Runtime engine is imported from the pure /dist/validation subpath (no server
6
+ // code), so it never drags Panel/MikroORM/Express into the browser bundle.
7
+ import { ValidationEngine } from '@maxal_studio/kratosjs/dist/validation';
8
+ import { evaluateCondition } from '../runtime/conditions';
9
+ import { useI18nContext } from '../i18n/I18nProvider';
10
+
11
+ /**
12
+ * Validation rule with optional condition
13
+ */
14
+ interface ValidationRuleWithCondition {
15
+ rule: ValidationRule;
16
+ condition?: boolean | string; // Serialized function string or boolean
17
+ }
18
+
19
+ /**
20
+ * Hook to convert KratosJs validation rules to React Hook Form format.
21
+ *
22
+ * Rule semantics are NOT re-implemented here: after resolving each rule's
23
+ * condition against the live form state, the surviving rules are handed to the
24
+ * shared `ValidationEngine` — the SAME engine the backend runs — via a single
25
+ * RHF `validate` function. This guarantees the client and server agree on every
26
+ * rule and message. `required` is additionally mapped to RHF's native
27
+ * `required` so the required-asterisk UI keeps working.
28
+ *
29
+ * @param rules Validation rules (strings, or objects with rule + condition)
30
+ * @param operation Current operation ('create' | 'edit' | 'view')
31
+ * @param fieldName Name of the field being validated (for cross-field rules and messages)
32
+ * @returns React Hook Form validation object
33
+ */
34
+ export function useValidation(
35
+ rules: (ValidationRule | ValidationRuleWithCondition)[] = [],
36
+ operation?: 'create' | 'edit' | 'view',
37
+ fieldName = '',
38
+ ): RHFValidationRules {
39
+ const { control } = useFormContext();
40
+ const formState = useWatch({ control }) || {};
41
+ const { t } = useI18nContext();
42
+
43
+ return useMemo(() => {
44
+ const validation: RHFValidationRules = {};
45
+
46
+ // Resolve conditions against the live form state, keeping only the
47
+ // rule strings that currently apply.
48
+ const activeRules: string[] = [];
49
+ rules.forEach(ruleOrObj => {
50
+ let rule: ValidationRule;
51
+ let condition: boolean | string | undefined;
52
+
53
+ if (typeof ruleOrObj === 'object' && ruleOrObj !== null && 'rule' in ruleOrObj) {
54
+ rule = ruleOrObj.rule;
55
+ condition = ruleOrObj.condition;
56
+ } else {
57
+ rule = ruleOrObj as ValidationRule;
58
+ }
59
+
60
+ if (condition !== undefined) {
61
+ const shouldApply = evaluateCondition(condition, formState, operation);
62
+ if (!shouldApply) return;
63
+ }
64
+
65
+ if (typeof rule === 'string') activeRules.push(rule);
66
+ });
67
+
68
+ // `required` → RHF native (drives the asterisk + empty-value enforcement).
69
+ if (activeRules.includes('required')) {
70
+ validation.required = t('core:validation.required_generic');
71
+ }
72
+
73
+ // Everything else is delegated to the shared engine. `required` is handled
74
+ // natively above, so it's excluded here to avoid a duplicate message.
75
+ const engineRules = activeRules.filter(r => r !== 'required');
76
+ if (engineRules.length > 0) {
77
+ validation.validate = {
78
+ ...((validation.validate as any) || {}),
79
+ kratos: (value: any, formValues: any) => {
80
+ const violations = ValidationEngine.validateValue(value, engineRules, {
81
+ allValues: formValues || {},
82
+ field: fieldName,
83
+ });
84
+ if (violations.length === 0) return true;
85
+ const v = violations[0];
86
+ // Render in the active locale when the rule provides an i18n key;
87
+ // otherwise use the already-rendered message (overrides/inline).
88
+ return v.messageKey ? t(`core:${v.messageKey}`, v.params) : v.message;
89
+ },
90
+ };
91
+ }
92
+
93
+ return validation;
94
+ }, [rules, formState, operation, fieldName, t]);
95
+ }
@@ -0,0 +1,128 @@
1
+ import React, { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
2
+ import type { KratosI18n } from '@maxal_studio/kratosjs/dist/i18n';
3
+ import { buildClientI18n, type ClientI18nConfig } from './buildClientI18n';
4
+ import { setActiveI18n } from './activeLocale';
5
+
6
+ const STORAGE_KEY = 'kratosjs-locale';
7
+
8
+ export interface I18nContextValue {
9
+ /** The underlying engine (escape hatch). */
10
+ engine: KratosI18n;
11
+ /** Active UI locale. */
12
+ locale: string;
13
+ /** Switch the active locale (persists + updates `<html lang/dir>` + re-fetches). */
14
+ setLocale: (locale: string) => void;
15
+ /** Supported locales. */
16
+ locales: string[];
17
+ /** Text direction of the active locale. */
18
+ dir: 'ltr' | 'rtl';
19
+ /** Translate a (possibly `ns:`-prefixed) key against the active locale. */
20
+ t: (key: string, params?: Record<string, unknown>) => string;
21
+ }
22
+
23
+ const I18nContext = createContext<I18nContextValue | null>(null);
24
+
25
+ // Lazy default engine so components used outside a provider (isolated tests, the
26
+ // pre-mount window) still resolve `core` chrome instead of crashing.
27
+ let defaultEngine: KratosI18n | null = null;
28
+ function getDefaultEngine(): KratosI18n {
29
+ if (!defaultEngine) defaultEngine = buildClientI18n();
30
+ return defaultEngine;
31
+ }
32
+
33
+ function resolveInitialLocale(engine: KratosI18n, configDefault?: string): string {
34
+ const locales = engine.getLocales();
35
+ const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null;
36
+ if (stored && locales.includes(stored)) return stored;
37
+ if (configDefault && locales.includes(configDefault)) return configDefault;
38
+ const nav = typeof navigator !== 'undefined' ? navigator.language : '';
39
+ if (nav) {
40
+ if (locales.includes(nav)) return nav;
41
+ const base = nav.split('-')[0];
42
+ if (locales.includes(base)) return base;
43
+ }
44
+ return engine.getDefaultLocale();
45
+ }
46
+
47
+ export interface I18nProviderProps {
48
+ config?: ClientI18nConfig;
49
+ children: React.ReactNode;
50
+ }
51
+
52
+ export function I18nProvider({ config, children }: I18nProviderProps) {
53
+ const engine = useMemo(() => buildClientI18n(config), [config]);
54
+ const [locale, setLocaleState] = useState(() => resolveInitialLocale(engine, config?.defaultLocale));
55
+
56
+ // Set the module mirror SYNCHRONOUSLY during render (not only in the effect) so
57
+ // non-hook `translate()` calls in children resolve the correct locale on the
58
+ // very first paint — important for a persisted non-default locale.
59
+ useMemo(() => setActiveI18n(engine, locale), [engine, locale]);
60
+
61
+ // Keep the <html> attributes + storage in sync with the locale.
62
+ useEffect(() => {
63
+ setActiveI18n(engine, locale);
64
+ if (typeof document !== 'undefined') {
65
+ document.documentElement.lang = locale;
66
+ document.documentElement.dir = engine.getDir(locale);
67
+ }
68
+ if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, locale);
69
+ }, [engine, locale]);
70
+
71
+ const setLocale = useCallback(
72
+ (next: string) => {
73
+ if (next === locale) return;
74
+ // Persist + apply the document attributes immediately, then do a full
75
+ // refresh so EVERY view — metadata, already-open table/form schemas, and
76
+ // server-rendered labels — re-fetches and re-renders in the new locale.
77
+ // (Schemas are cached per-mount, so a targeted re-fetch would miss them.)
78
+ if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, next);
79
+ if (typeof document !== 'undefined') {
80
+ document.documentElement.lang = next;
81
+ document.documentElement.dir = engine.getDir(next);
82
+ }
83
+ try {
84
+ if (typeof window !== 'undefined' && typeof window.location?.reload === 'function') {
85
+ window.location.reload();
86
+ return;
87
+ }
88
+ } catch {
89
+ // jsdom/SSR: reload isn't implemented — fall through to in-place update.
90
+ }
91
+ setLocaleState(next);
92
+ },
93
+ [locale, engine],
94
+ );
95
+
96
+ const value = useMemo<I18nContextValue>(
97
+ () => ({
98
+ engine,
99
+ locale,
100
+ setLocale,
101
+ locales: engine.getLocales(),
102
+ dir: engine.getDir(locale),
103
+ t: (key, params) => engine.t(key, { locale, ...params }),
104
+ }),
105
+ [engine, locale, setLocale],
106
+ );
107
+
108
+ return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
109
+ }
110
+
111
+ /**
112
+ * Read the i18n context. Degrades gracefully to a default `core`-only engine when
113
+ * no `<I18nProvider>` is mounted (so isolated component tests don't all need wrapping).
114
+ */
115
+ export function useI18nContext(): I18nContextValue {
116
+ const ctx = useContext(I18nContext);
117
+ if (ctx) return ctx;
118
+ const engine = getDefaultEngine();
119
+ const locale = engine.getDefaultLocale();
120
+ return {
121
+ engine,
122
+ locale,
123
+ setLocale: () => {},
124
+ locales: engine.getLocales(),
125
+ dir: engine.getDir(locale),
126
+ t: (key, params) => engine.t(key, { locale, ...params }),
127
+ };
128
+ }
@@ -0,0 +1,50 @@
1
+ import { useI18nContext } from './I18nProvider';
2
+
3
+ /** Human label for a locale code, in the locale's own language (falls back to the code). */
4
+ function localeLabel(code: string): string {
5
+ try {
6
+ const dn = new Intl.DisplayNames([code], { type: 'language' });
7
+ const name = dn.of(code);
8
+ if (name) return name.charAt(0).toUpperCase() + name.slice(1);
9
+ } catch {
10
+ // Intl.DisplayNames unavailable — fall through.
11
+ }
12
+ return code;
13
+ }
14
+
15
+ export interface LocaleSwitcherProps {
16
+ className?: string;
17
+ /** Accessible label (defaults to the translated `core:panel.language`). */
18
+ 'aria-label'?: string;
19
+ }
20
+
21
+ /**
22
+ * Native `<select>` locale switcher. Renders nothing when the panel has a single
23
+ * locale. Used in the Header account menu and on the login screen.
24
+ */
25
+ export function LocaleSwitcher({ className, ...rest }: LocaleSwitcherProps) {
26
+ const { locale, setLocale, locales, t } = useI18nContext();
27
+ if (locales.length <= 1) return null;
28
+
29
+ return (
30
+ <select
31
+ value={locale}
32
+ onChange={e => setLocale(e.target.value)}
33
+ aria-label={rest['aria-label'] ?? t('core:panel.language')}
34
+ className={
35
+ className ??
36
+ 'k-input text-sm py-1.5 px-3 pr-8 rounded-md border border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-accent disabled:opacity-50 disabled:cursor-not-allowed transition-colors appearance-none bg-no-repeat bg-right'
37
+ }
38
+ style={{
39
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E")`,
40
+ backgroundPosition: 'right 0.5rem center',
41
+ backgroundSize: '1.5em 1.5em',
42
+ }}>
43
+ {locales.map(code => (
44
+ <option key={code} value={code}>
45
+ {localeLabel(code)}
46
+ </option>
47
+ ))}
48
+ </select>
49
+ );
50
+ }
@@ -0,0 +1,39 @@
1
+ // Module-level mirror of the active client locale + engine.
2
+ //
3
+ // Lets non-hook utilities (formatValue, tableFormatters) localize via `translate`
4
+ // / `getActiveLocale` without threading a locale argument through every call.
5
+ // The I18nProvider keeps this in sync with React state on every locale change.
6
+
7
+ import type { KratosI18n } from '@maxal_studio/kratosjs/dist/i18n';
8
+ import { buildClientI18n } from './buildClientI18n';
9
+
10
+ let activeEngine: KratosI18n | null = null;
11
+ let activeLocale = 'en';
12
+ let defaultEngine: KratosI18n | null = null;
13
+
14
+ /** The active engine, or a lazily-built `core`-only default (no provider mounted). */
15
+ function engine(): KratosI18n {
16
+ if (activeEngine) return activeEngine;
17
+ if (!defaultEngine) defaultEngine = buildClientI18n();
18
+ return defaultEngine;
19
+ }
20
+
21
+ /** Update the active engine + locale (called by I18nProvider). */
22
+ export function setActiveI18n(next: KratosI18n, locale: string): void {
23
+ activeEngine = next;
24
+ activeLocale = locale;
25
+ }
26
+
27
+ /** The active locale, e.g. for `Intl` formatters. Defaults to `'en'`. */
28
+ export function getActiveLocale(): string {
29
+ return activeLocale;
30
+ }
31
+
32
+ /**
33
+ * Translate `key` against the active locale. Falls back to a `core`-only default
34
+ * engine when no provider has mounted (so isolated tests + pre-mount utilities
35
+ * still resolve chrome). Prefer `useTranslation` inside components.
36
+ */
37
+ export function translate(key: string, params?: Record<string, unknown>): string {
38
+ return engine().t(key, { locale: activeLocale, ...params });
39
+ }
@@ -0,0 +1,101 @@
1
+ // Build the client KratosI18n from the server-injected config + optional overrides.
2
+ //
3
+ // The backend is the single source of truth: it injects the plugin + app catalogs
4
+ // (and the locale config) into the admin HTML, and the client consumes them here.
5
+ //
6
+ // Catalog precedence (later wins): package chrome (`core`) → server-injected
7
+ // resources (plugin + app namespaces) → optional mount-time overrides. App override
8
+ // catalogs may target any namespace via a `ns:` key prefix (e.g. `core:common.save`
9
+ // overrides a built-in chrome string); unprefixed keys go to the `app` namespace.
10
+
11
+ import {
12
+ createI18n,
13
+ type KratosI18n,
14
+ type I18nResources,
15
+ type Catalog,
16
+ type Direction,
17
+ } from '@maxal_studio/kratosjs/dist/i18n';
18
+ import { clientCoreResources } from './locales/core';
19
+
20
+ /** Per-locale catalog map, e.g. `{ en: {...}, sq: {...} }`. */
21
+ export type ClientTranslations = Record<string, Catalog>;
22
+
23
+ export interface ClientI18nConfig {
24
+ defaultLocale?: string;
25
+ fallbackLocale?: string;
26
+ locales?: string[];
27
+ /** Text direction per locale, forwarded from the server for app-added RTL locales. */
28
+ directions?: Record<string, Direction>;
29
+ /**
30
+ * Server-injected catalogs (`namespace -> locale -> catalog`) for the plugin and
31
+ * app namespaces. Populated from `window.__VALAJS_I18N__`.
32
+ */
33
+ resources?: I18nResources;
34
+ /**
35
+ * Optional mount-time override catalogs by locale. Keys may be `ns:key`
36
+ * (defaults to the `app` namespace) — mainly to override built-in `core:` chrome.
37
+ */
38
+ translations?: ClientTranslations;
39
+ }
40
+
41
+ function mergeInto(target: I18nResources, namespace: string, locale: string, catalog: Catalog): void {
42
+ const ns = (target[namespace] ??= {});
43
+ ns[locale] = { ...(ns[locale] ?? {}), ...catalog };
44
+ }
45
+
46
+ /**
47
+ * Assemble the `namespace -> locale -> catalog` resources for the client engine.
48
+ */
49
+ export function buildClientResources(config: ClientI18nConfig = {}): I18nResources {
50
+ const resources: I18nResources = {};
51
+
52
+ // 1. Package chrome (core namespace) — bundled with the React package.
53
+ for (const [locale, catalog] of Object.entries(clientCoreResources)) {
54
+ mergeInto(resources, 'core', locale, catalog);
55
+ }
56
+
57
+ // 2. Server-injected catalogs (plugin + app namespaces, already in the right
58
+ // app-wins precedence from the backend merge).
59
+ for (const [namespace, byLocale] of Object.entries(config.resources ?? {})) {
60
+ for (const [locale, catalog] of Object.entries(byLocale)) {
61
+ mergeInto(resources, namespace, locale, catalog);
62
+ }
63
+ }
64
+
65
+ // 3. Mount-time overrides (last → win). A `ns:` prefix routes the key to that
66
+ // namespace, so the app can override core/plugin strings; otherwise `app`.
67
+ for (const [locale, catalog] of Object.entries(config.translations ?? {})) {
68
+ for (const [key, value] of Object.entries(catalog)) {
69
+ const idx = key.indexOf(':');
70
+ const namespace = idx === -1 ? 'app' : key.slice(0, idx);
71
+ const realKey = idx === -1 ? key : key.slice(idx + 1);
72
+ mergeInto(resources, namespace, locale, { [realKey]: value });
73
+ }
74
+ }
75
+
76
+ return resources;
77
+ }
78
+
79
+ /** Locales discovered across all registered catalogs (plus any declared). */
80
+ export function discoverClientLocales(resources: I18nResources, declared?: string[]): string[] {
81
+ if (declared && declared.length > 0) return declared;
82
+ const set = new Set<string>();
83
+ for (const byLocale of Object.values(resources)) {
84
+ for (const locale of Object.keys(byLocale)) set.add(locale);
85
+ }
86
+ return set.size > 0 ? [...set] : ['en'];
87
+ }
88
+
89
+ /** Create the client engine from the server-injected config + optional overrides. */
90
+ export function buildClientI18n(config: ClientI18nConfig = {}): KratosI18n {
91
+ const resources = buildClientResources(config);
92
+ const locales = discoverClientLocales(resources, config.locales);
93
+ const defaultLocale = config.defaultLocale ?? (locales.includes('en') ? 'en' : locales[0]);
94
+ return createI18n({
95
+ locales,
96
+ defaultLocale,
97
+ fallbackLocale: config.fallbackLocale ?? defaultLocale,
98
+ directions: config.directions,
99
+ resources,
100
+ });
101
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it, beforeEach, vi } from 'vitest';
2
+ import React from 'react';
3
+ import { render, screen, renderHook, act } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { I18nProvider } from './I18nProvider';
6
+ import { useTranslation } from './useTranslation';
7
+ import { useLocale } from './useLocale';
8
+ import { useFormatter } from './useFormatter';
9
+ import { LocaleSwitcher } from './LocaleSwitcher';
10
+ import { buildClientResources, buildClientI18n } from './buildClientI18n';
11
+
12
+ const config = {
13
+ defaultLocale: 'en',
14
+ translations: {
15
+ en: { 'home.title': 'Home', 'core:common.save': 'Save it' },
16
+ sq: { 'home.title': 'Ballina', 'core:common.save': 'Ruaje' },
17
+ },
18
+ };
19
+
20
+ function wrapper({ children }: { children: React.ReactNode }) {
21
+ return <I18nProvider config={config}>{children}</I18nProvider>;
22
+ }
23
+
24
+ beforeEach(() => localStorage.clear());
25
+
26
+ describe('buildClientResources', () => {
27
+ it('routes app keys: bare → app namespace, ns: prefix → that namespace', () => {
28
+ const res = buildClientResources(config);
29
+ expect(res.app.en['home.title']).toBe('Home');
30
+ // `core:common.save` overrides the package chrome default.
31
+ expect(res.core.en['common.save']).toBe('Save it');
32
+ // Built-in chrome still present for non-overridden keys.
33
+ expect(res.core.en['common.cancel']).toBe('Cancel');
34
+ });
35
+
36
+ it('merges server-injected resources (plugin + app namespaces)', () => {
37
+ const res = buildClientResources({
38
+ resources: {
39
+ twofa: { en: { title: 'Verify' } },
40
+ app: { en: { 'users.label': 'Users' } },
41
+ },
42
+ });
43
+ expect(res.twofa.en['title']).toBe('Verify');
44
+ expect(res.app.en['users.label']).toBe('Users');
45
+ // Built-in chrome is still present alongside injected catalogs.
46
+ expect(res.core.en['common.cancel']).toBe('Cancel');
47
+ });
48
+
49
+ it('lets mount-time translations override server-injected resources', () => {
50
+ const res = buildClientResources({
51
+ resources: { app: { en: { 'users.label': 'Users' } } },
52
+ translations: { en: { 'users.label': 'People' } },
53
+ });
54
+ expect(res.app.en['users.label']).toBe('People');
55
+ });
56
+
57
+ it('localizes built-in chrome for a custom locale via injected `core` resources', () => {
58
+ // Backend registered `core` chrome for French (a locale the package does not bundle)
59
+ // plus an English override — both arrive via the injected resources.
60
+ const res = buildClientResources({
61
+ locales: ['en', 'fr'],
62
+ resources: { core: { fr: { 'common.save': 'Enregistrer' }, en: { 'common.save': 'Store' } } },
63
+ });
64
+ // New locale gets chrome from the injected catalog.
65
+ expect(res.core.fr['common.save']).toBe('Enregistrer');
66
+ // Injected `core` overrides the bundled default for existing locales...
67
+ expect(res.core.en['common.save']).toBe('Store');
68
+ // ...while non-overridden bundled chrome keys stay intact.
69
+ expect(res.core.en['common.cancel']).toBe('Cancel');
70
+ });
71
+
72
+ it('resolves chrome in a custom locale end-to-end through the engine', () => {
73
+ const engine = buildClientI18n({
74
+ locales: ['en', 'fr'],
75
+ defaultLocale: 'en',
76
+ resources: { core: { fr: { 'common.save': 'Enregistrer' } } },
77
+ });
78
+ expect(engine.t('core:common.save', { locale: 'fr' })).toBe('Enregistrer');
79
+ expect(engine.getLocales()).toContain('fr');
80
+ });
81
+ });
82
+
83
+ describe('useTranslation', () => {
84
+ it('translates chrome + app keys (app overrides core)', () => {
85
+ const { result } = renderHook(() => useTranslation(), { wrapper });
86
+ expect(result.current.t('app:home.title')).toBe('Home');
87
+ expect(result.current.t('core:common.save')).toBe('Save it'); // app override
88
+ expect(result.current.t('core:common.cancel')).toBe('Cancel'); // package default
89
+ });
90
+
91
+ it('switching locale persists the choice and triggers a full refresh', () => {
92
+ const reload = vi.fn();
93
+ Object.defineProperty(window, 'location', { value: { reload }, writable: true });
94
+ const { result } = renderHook(() => useLocale(), { wrapper });
95
+ act(() => result.current.setLocale('sq'));
96
+ expect(localStorage.getItem('kratosjs-locale')).toBe('sq');
97
+ expect(document.documentElement.lang).toBe('sq');
98
+ expect(reload).toHaveBeenCalled();
99
+ });
100
+
101
+ it('falls back to a default engine outside any provider', () => {
102
+ const { result } = renderHook(() => useTranslation());
103
+ expect(result.current.t('core:common.save')).toBe('Save'); // built-in default
104
+ });
105
+ });
106
+
107
+ describe('LocaleSwitcher', () => {
108
+ it('renders the registered locales and switches on change', async () => {
109
+ const reload = vi.fn();
110
+ Object.defineProperty(window, 'location', { value: { reload }, writable: true });
111
+ render(
112
+ <I18nProvider config={config}>
113
+ <LocaleSwitcher />
114
+ </I18nProvider>,
115
+ );
116
+ const select = screen.getByRole('combobox');
117
+ expect(select).toBeInTheDocument();
118
+ await userEvent.selectOptions(select, 'sq');
119
+ expect(localStorage.getItem('kratosjs-locale')).toBe('sq');
120
+ expect(document.documentElement.lang).toBe('sq');
121
+ expect(reload).toHaveBeenCalled();
122
+ });
123
+
124
+ it('renders nothing for a single-locale panel', () => {
125
+ render(
126
+ <I18nProvider config={{ defaultLocale: 'en', locales: ['en'], translations: { en: {} } }}>
127
+ <LocaleSwitcher />
128
+ </I18nProvider>,
129
+ );
130
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
131
+ });
132
+ });
133
+
134
+ describe('useFormatter', () => {
135
+ it('formats currency and relative time in the active locale', () => {
136
+ const { result } = renderHook(() => useFormatter(), { wrapper });
137
+ expect(result.current.currency(1234.5, 'USD')).toContain('1,234.50');
138
+ expect(result.current.relativeTime(-2, 'hour')).toBe('2 hours ago');
139
+ });
140
+ });