@alepha/ui 0.14.1 → 0.14.3

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 (183) hide show
  1. package/dist/admin/AdminAudits-B3EhKhN7.js +3 -0
  2. package/dist/admin/{AdminAudits-CwvH8e8c.js → AdminAudits-DIrCCPk3.js} +3 -2
  3. package/dist/admin/AdminAudits-DIrCCPk3.js.map +1 -0
  4. package/dist/admin/AdminFiles-C8OG4dtD.js +3 -0
  5. package/dist/admin/{AdminFiles-C_w1tb_x.js → AdminFiles-RsL178Ta.js} +2 -2
  6. package/dist/admin/{AdminFiles-C_w1tb_x.js.map → AdminFiles-RsL178Ta.js.map} +1 -1
  7. package/dist/admin/AdminNotifications-BSL4B2fQ.js +3 -0
  8. package/dist/admin/{AdminNotifications-DuYy74AN.js → AdminNotifications-cIbywWKi.js} +2 -2
  9. package/dist/admin/AdminNotifications-cIbywWKi.js.map +1 -0
  10. package/dist/admin/{AdminParameters-DYg48Jwe.js → AdminParameters-BKObzzpN.js} +1 -1
  11. package/dist/admin/{AdminParameters-YagqWTG3.js → AdminParameters-D-q3Qmhv.js} +2 -2
  12. package/dist/admin/{AdminParameters-YagqWTG3.js.map → AdminParameters-D-q3Qmhv.js.map} +1 -1
  13. package/dist/admin/AdminSessions-DHG9zPfr.js +3 -0
  14. package/dist/admin/{AdminSessions-BCjgJ-93.js → AdminSessions-vOgkrQ2U.js} +3 -2
  15. package/dist/admin/AdminSessions-vOgkrQ2U.js.map +1 -0
  16. package/dist/admin/{AdminUserAudits-B_PUXCKC.js → AdminUserAudits-CSsN1fIC.js} +3 -2
  17. package/dist/admin/AdminUserAudits-CSsN1fIC.js.map +1 -0
  18. package/dist/admin/{AdminUserAudits-D7cTcElL.js → AdminUserAudits-DmAnivo3.js} +1 -1
  19. package/dist/admin/{AdminUserCreate-DzfRbGZ4.js → AdminUserCreate-B72nu-3W.js} +3 -2
  20. package/dist/admin/AdminUserCreate-B72nu-3W.js.map +1 -0
  21. package/dist/admin/{AdminUserCreate-oUA1KDIl.js → AdminUserCreate-DpA13zwj.js} +1 -1
  22. package/dist/admin/AdminUserDetails-CKM2IEMr.js +475 -0
  23. package/dist/admin/AdminUserDetails-CKM2IEMr.js.map +1 -0
  24. package/dist/admin/{AdminUserDetails-y1H5DW8Y.js → AdminUserDetails-Zib_B6Al.js} +1 -1
  25. package/dist/admin/{AdminUserLayout-Dejnz13m.js → AdminUserLayout-BNBOEiAO.js} +1 -1
  26. package/dist/admin/AdminUserLayout-D7En9UBq.js +334 -0
  27. package/dist/admin/AdminUserLayout-D7En9UBq.js.map +1 -0
  28. package/dist/admin/AdminUserSessions-D9X2_HMA.js +3 -0
  29. package/dist/admin/{AdminUserSessions-DO9H85O-.js → AdminUserSessions-DEaGu6n6.js} +3 -2
  30. package/dist/admin/AdminUserSessions-DEaGu6n6.js.map +1 -0
  31. package/dist/admin/{AdminUserSettings-B3jA8g3p.js → AdminUserSettings-Di73D7g2.js} +8 -6
  32. package/dist/admin/AdminUserSettings-Di73D7g2.js.map +1 -0
  33. package/dist/admin/AdminUserSettings-yI-JECf5.js +3 -0
  34. package/dist/admin/{AdminUsers-ebbrJBT0.js → AdminUsers-BnGIRvmV.js} +3 -2
  35. package/dist/admin/AdminUsers-BnGIRvmV.js.map +1 -0
  36. package/dist/admin/AdminUsers-CG9-2Z8W.js +3 -0
  37. package/dist/admin/index.d.ts +25 -25
  38. package/dist/admin/index.d.ts.map +1 -1
  39. package/dist/admin/index.js +37 -36
  40. package/dist/admin/index.js.map +1 -1
  41. package/dist/auth/{AuthLayout-BAZJHzDG.js → AuthLayout-B1sUB8fB.js} +2 -2
  42. package/dist/auth/AuthLayout-B1sUB8fB.js.map +1 -0
  43. package/dist/auth/Login-BWi-pPbO.js +4 -0
  44. package/dist/auth/{Login-CeNZZjrr.js → Login-Cjxv3EDi.js} +2 -2
  45. package/dist/auth/Login-Cjxv3EDi.js.map +1 -0
  46. package/dist/auth/{Register-s4ENeyiE.js → Register-BKBIpHhW.js} +3 -2
  47. package/dist/auth/Register-BKBIpHhW.js.map +1 -0
  48. package/dist/auth/Register-CtdvihIM.js +4 -0
  49. package/dist/auth/ResetPassword-BUdM7T_R.js +3 -0
  50. package/dist/auth/{ResetPassword-GLIFkJT7.js → ResetPassword-DvqD_1SJ.js} +3 -2
  51. package/dist/auth/ResetPassword-DvqD_1SJ.js.map +1 -0
  52. package/dist/auth/VerifyEmail-BYmtnkEl.js +3 -0
  53. package/dist/auth/{VerifyEmail-R79sSej_.js → VerifyEmail-VaBruOnO.js} +3 -2
  54. package/dist/auth/VerifyEmail-VaBruOnO.js.map +1 -0
  55. package/dist/auth/index.d.ts +11 -11
  56. package/dist/auth/index.d.ts.map +1 -1
  57. package/dist/auth/index.js +10 -10
  58. package/dist/auth/index.js.map +1 -1
  59. package/dist/core/index.d.ts +36 -55
  60. package/dist/core/index.d.ts.map +1 -1
  61. package/dist/core/index.js +50 -350
  62. package/dist/core/index.js.map +1 -1
  63. package/dist/demo/DemoDataTable-2mzzf__a.js +150 -0
  64. package/dist/demo/DemoDataTable-2mzzf__a.js.map +1 -0
  65. package/dist/demo/DemoHome-CnuL5WV9.js +25 -0
  66. package/dist/demo/DemoHome-CnuL5WV9.js.map +1 -0
  67. package/dist/demo/DemoHome-D6Z7EE4V.js +3 -0
  68. package/dist/demo/DemoJsonViewer-CYUggLop.js +4 -0
  69. package/dist/demo/DemoJsonViewer-NUGst5wW.js +430 -0
  70. package/dist/demo/DemoJsonViewer-NUGst5wW.js.map +1 -0
  71. package/dist/demo/DemoLayout-ZFDzyvY3.js +3 -0
  72. package/dist/demo/DemoLayout-dvbeuBBf.js +47 -0
  73. package/dist/demo/DemoLayout-dvbeuBBf.js.map +1 -0
  74. package/dist/demo/DemoLogin--wE44i23.js +327 -0
  75. package/dist/demo/DemoLogin--wE44i23.js.map +1 -0
  76. package/dist/demo/DemoRegister-BtrMksx6.js +488 -0
  77. package/dist/demo/DemoRegister-BtrMksx6.js.map +1 -0
  78. package/dist/demo/DemoResetPassword-DVXiiiX7.js +341 -0
  79. package/dist/demo/DemoResetPassword-DVXiiiX7.js.map +1 -0
  80. package/dist/demo/DemoSidebar-DWnjYHoP.js +82 -0
  81. package/dist/demo/DemoSidebar-DWnjYHoP.js.map +1 -0
  82. package/dist/demo/DemoTypeForm-P5_VInW2.js +83 -0
  83. package/dist/demo/DemoTypeForm-P5_VInW2.js.map +1 -0
  84. package/dist/demo/DemoVerifyEmail-C_ooC5u8.js +152 -0
  85. package/dist/demo/DemoVerifyEmail-C_ooC5u8.js.map +1 -0
  86. package/dist/demo/IconGoogle-DvmFiEDB.js +58 -0
  87. package/dist/demo/IconGoogle-DvmFiEDB.js.map +1 -0
  88. package/dist/demo/Showcase-vemLuO2t.js +187 -0
  89. package/dist/demo/Showcase-vemLuO2t.js.map +1 -0
  90. package/dist/demo/index.d.ts +97 -0
  91. package/dist/demo/index.d.ts.map +1 -0
  92. package/dist/demo/index.js +121 -0
  93. package/dist/demo/index.js.map +1 -0
  94. package/dist/json/index.d.ts +58 -0
  95. package/dist/json/index.d.ts.map +1 -0
  96. package/dist/json/index.js +325 -0
  97. package/dist/json/index.js.map +1 -0
  98. package/package.json +25 -14
  99. package/src/admin/AdminRouter.ts +23 -20
  100. package/src/admin/MainRouter.ts +2 -2
  101. package/src/admin/components/audits/AdminAudits.tsx +4 -3
  102. package/src/admin/components/jobs/AdminJobs.tsx +2 -2
  103. package/src/admin/components/notifications/AdminNotifications.tsx +2 -2
  104. package/src/admin/components/parameters/AdminParameters.tsx +2 -2
  105. package/src/admin/components/sessions/AdminSessions.tsx +4 -3
  106. package/src/admin/components/shared/AdminResourceHeader.tsx +281 -0
  107. package/src/admin/components/shared/AdminResourceTabs.tsx +94 -0
  108. package/src/admin/components/shared/index.ts +10 -0
  109. package/src/admin/components/users/AdminUserAudits.tsx +4 -3
  110. package/src/admin/components/users/AdminUserCreate.tsx +4 -3
  111. package/src/admin/components/users/AdminUserDetails.tsx +339 -86
  112. package/src/admin/components/users/AdminUserLayout.tsx +165 -113
  113. package/src/admin/components/users/AdminUserSessions.tsx +4 -3
  114. package/src/admin/components/users/AdminUserSettings.tsx +12 -6
  115. package/src/admin/components/users/AdminUsers.tsx +8 -3
  116. package/src/auth/AuthRouter.ts +1 -1
  117. package/src/auth/components/AuthLayout.tsx +1 -1
  118. package/src/auth/components/Login.tsx +1 -1
  119. package/src/auth/components/Register.tsx +2 -1
  120. package/src/auth/components/ResetPassword.tsx +2 -1
  121. package/src/auth/components/VerifyEmail.tsx +2 -1
  122. package/src/auth/components/buttons/UserButton.tsx +1 -1
  123. package/src/core/RootRouter.ts +1 -1
  124. package/src/core/components/buttons/ActionButton.tsx +3 -4
  125. package/src/core/components/form/Control.tsx +12 -1
  126. package/src/core/components/form/ControlNumber.tsx +5 -0
  127. package/src/core/components/form/TypeForm.tsx +3 -2
  128. package/src/core/components/layout/AdminShell.tsx +2 -1
  129. package/src/core/components/layout/AlephaMantineProvider.tsx +7 -2
  130. package/src/core/components/layout/Omnibar.tsx +2 -1
  131. package/src/core/components/layout/Sidebar.tsx +18 -18
  132. package/src/core/index.ts +1 -2
  133. package/src/core/services/DialogService.tsx +0 -17
  134. package/{styles.css → src/core/styles.css} +1 -5
  135. package/src/demo/DemoRouter.ts +123 -0
  136. package/src/demo/components/DemoHome.tsx +29 -0
  137. package/src/demo/components/DemoLayout.tsx +52 -0
  138. package/src/demo/components/auth/DemoLogin.tsx +130 -0
  139. package/src/demo/components/auth/DemoRegister.tsx +144 -0
  140. package/src/demo/components/auth/DemoResetPassword.tsx +69 -0
  141. package/src/demo/components/auth/DemoVerifyEmail.tsx +28 -0
  142. package/src/demo/components/core/DemoDataTable.tsx +174 -0
  143. package/src/demo/components/core/DemoSidebar.tsx +85 -0
  144. package/src/demo/components/core/DemoTypeForm.tsx +69 -0
  145. package/src/demo/components/json/DemoJsonViewer.tsx +128 -0
  146. package/src/demo/components/shared/MacWindow.tsx +105 -0
  147. package/src/demo/components/shared/Showcase.tsx +112 -0
  148. package/src/demo/index.ts +30 -0
  149. package/src/demo/styles.css +0 -0
  150. package/src/json/components/JsonViewer.css +25 -0
  151. package/src/json/components/JsonViewer.tsx +526 -0
  152. package/src/json/extensions/DialogService.tsx +31 -0
  153. package/src/json/index.ts +5 -0
  154. package/src/json/styles.css +1 -0
  155. package/dist/admin/AdminAudits-CwvH8e8c.js.map +0 -1
  156. package/dist/admin/AdminAudits-Dv8Vk_6r.js +0 -3
  157. package/dist/admin/AdminFiles-5CPA3lQk.js +0 -3
  158. package/dist/admin/AdminNotifications-DLjmZWtf.js +0 -3
  159. package/dist/admin/AdminNotifications-DuYy74AN.js.map +0 -1
  160. package/dist/admin/AdminSessions-BCjgJ-93.js.map +0 -1
  161. package/dist/admin/AdminSessions-DEh2uN-4.js +0 -3
  162. package/dist/admin/AdminUserAudits-B_PUXCKC.js.map +0 -1
  163. package/dist/admin/AdminUserCreate-DzfRbGZ4.js.map +0 -1
  164. package/dist/admin/AdminUserDetails-DeTrJm-t.js +0 -221
  165. package/dist/admin/AdminUserDetails-DeTrJm-t.js.map +0 -1
  166. package/dist/admin/AdminUserLayout-CsfrrZkD.js +0 -150
  167. package/dist/admin/AdminUserLayout-CsfrrZkD.js.map +0 -1
  168. package/dist/admin/AdminUserSessions-Bbhcpz4k.js +0 -3
  169. package/dist/admin/AdminUserSessions-DO9H85O-.js.map +0 -1
  170. package/dist/admin/AdminUserSettings-B3jA8g3p.js.map +0 -1
  171. package/dist/admin/AdminUserSettings-CE0xpbQc.js +0 -3
  172. package/dist/admin/AdminUsers-CegGZDhW.js +0 -3
  173. package/dist/admin/AdminUsers-ebbrJBT0.js.map +0 -1
  174. package/dist/auth/AuthLayout-BAZJHzDG.js.map +0 -1
  175. package/dist/auth/Login-CeNZZjrr.js.map +0 -1
  176. package/dist/auth/Login-hQcu1nlu.js +0 -4
  177. package/dist/auth/Register-B6HBNVHS.js +0 -4
  178. package/dist/auth/Register-s4ENeyiE.js.map +0 -1
  179. package/dist/auth/ResetPassword-Cjd-W-Nu.js +0 -3
  180. package/dist/auth/ResetPassword-GLIFkJT7.js.map +0 -1
  181. package/dist/auth/VerifyEmail-Dc9ABKUw.js +0 -3
  182. package/dist/auth/VerifyEmail-R79sSej_.js.map +0 -1
  183. package/src/core/components/data/JsonViewer.tsx +0 -361
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/demo/DemoRouter.ts","../../src/demo/index.ts"],"sourcesContent":["import { $page } from \"@alepha/react/router\";\nimport {\n IconBinaryTree,\n IconBraces,\n IconForms,\n IconHome,\n IconKey,\n IconLayoutSidebar,\n IconLockQuestion,\n IconLogin,\n IconMailCheck,\n IconPackages,\n IconTable,\n IconUserPlus,\n IconWall,\n} from \"@tabler/icons-react\";\n\nexport class DemoRouter {\n demoLayout = $page({\n icon: IconPackages,\n path: \"/demo\",\n label: \"Demo\",\n lazy: () => import(\"./components/DemoLayout.tsx\"),\n children: () => [\n this.demoHome,\n this.demoCore,\n this.demoJson,\n this.demoAuth,\n ],\n });\n\n demoHome = $page({\n icon: IconHome,\n path: \"/\",\n label: \"Home\",\n lazy: () => import(\"./components/DemoHome.tsx\"),\n });\n\n // Core Components\n demoCore = $page({\n icon: IconWall,\n path: \"/core\",\n label: \"Core\",\n children: () => [this.demoTypeForm, this.demoSidebar, this.demoDataTable],\n });\n\n demoTypeForm = $page({\n icon: IconForms,\n path: \"/type-form\",\n label: \"TypeForm\",\n lazy: () => import(\"./components/core/DemoTypeForm.tsx\"),\n });\n\n demoSidebar = $page({\n icon: IconLayoutSidebar,\n path: \"/sidebar\",\n label: \"Sidebar\",\n lazy: () => import(\"./components/core/DemoSidebar.tsx\"),\n });\n\n demoDataTable = $page({\n icon: IconTable,\n path: \"/data-table\",\n label: \"DataTable\",\n lazy: () => import(\"./components/core/DemoDataTable.tsx\"),\n });\n\n // JSON Components\n demoJson = $page({\n icon: IconBraces,\n path: \"/json\",\n label: \"Json\",\n children: () => [this.demoJsonViewer],\n });\n\n demoJsonViewer = $page({\n icon: IconBinaryTree,\n path: \"/viewer\",\n label: \"JsonViewer\",\n lazy: () => import(\"./components/json/DemoJsonViewer.tsx\"),\n });\n\n // Auth Components\n demoAuth = $page({\n icon: IconKey,\n path: \"/auth\",\n label: \"Auth\",\n children: () => [\n this.demoLogin,\n this.demoRegister,\n this.demoResetPassword,\n this.demoVerifyEmail,\n ],\n });\n\n demoLogin = $page({\n icon: IconLogin,\n path: \"/login\",\n label: \"Login\",\n lazy: () => import(\"./components/auth/DemoLogin.tsx\"),\n });\n\n demoRegister = $page({\n icon: IconUserPlus,\n path: \"/register\",\n label: \"Register\",\n lazy: () => import(\"./components/auth/DemoRegister.tsx\"),\n });\n\n demoResetPassword = $page({\n icon: IconLockQuestion,\n path: \"/reset-password\",\n label: \"ResetPassword\",\n lazy: () => import(\"./components/auth/DemoResetPassword.tsx\"),\n });\n\n demoVerifyEmail = $page({\n icon: IconMailCheck,\n path: \"/verify-email\",\n label: \"VerifyEmail\",\n lazy: () => import(\"./components/auth/DemoVerifyEmail.tsx\"),\n });\n}\n","import { AlephaUI } from \"@alepha/ui\";\nimport { $module } from \"alepha\";\nimport { DemoRouter } from \"./DemoRouter.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport { default as DemoHome } from \"./components/DemoHome.tsx\";\nexport { default as DemoLayout } from \"./components/DemoLayout.tsx\";\nexport { default as DemoJsonViewer } from \"./components/json/DemoJsonViewer.tsx\";\nexport {\n default as MacWindow,\n type MacWindowProps,\n} from \"./components/shared/MacWindow.tsx\";\nexport {\n default as Showcase,\n type ShowcaseProps,\n} from \"./components/shared/Showcase.tsx\";\nexport { DemoRouter } from \"./DemoRouter.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Demo UI Module - Component showcase and documentation\n *\n * @module alepha.ui.demo\n */\nexport const AlephaUIDemo = $module({\n name: \"alepha.ui.demo\",\n services: [AlephaUI, DemoRouter],\n});\n"],"mappings":";;;;;;;;;;AAiBA,IAAa,aAAb,MAAwB;CACtB,aAAa,MAAM;EACjB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACnB,gBAAgB;GACd,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACN;EACF,CAAC;CAEF,WAAW,MAAM;EACf,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAGF,WAAW,MAAM;EACf,MAAM;EACN,MAAM;EACN,OAAO;EACP,gBAAgB;GAAC,KAAK;GAAc,KAAK;GAAa,KAAK;GAAc;EAC1E,CAAC;CAEF,eAAe,MAAM;EACnB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAEF,cAAc,MAAM;EAClB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAEF,gBAAgB,MAAM;EACpB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAGF,WAAW,MAAM;EACf,MAAM;EACN,MAAM;EACN,OAAO;EACP,gBAAgB,CAAC,KAAK,eAAe;EACtC,CAAC;CAEF,iBAAiB,MAAM;EACrB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAGF,WAAW,MAAM;EACf,MAAM;EACN,MAAM;EACN,OAAO;EACP,gBAAgB;GACd,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACN;EACF,CAAC;CAEF,YAAY,MAAM;EAChB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAEF,eAAe,MAAM;EACnB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAEF,oBAAoB,MAAM;EACxB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;CAEF,kBAAkB,MAAM;EACtB,MAAM;EACN,MAAM;EACN,OAAO;EACP,YAAY,OAAO;EACpB,CAAC;;;;;;;;;;AC/FJ,MAAa,eAAe,QAAQ;CAClC,MAAM;CACN,UAAU,CAAC,UAAU,WAAW;CACjC,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { BaseDialogOptions } from "@alepha/ui";
2
+ import { MantineSize } from "@mantine/core";
3
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
+
5
+ //#region ../../src/json/extensions/DialogService.d.ts
6
+ declare module "@alepha/ui" {
7
+ interface DialogService {
8
+ /**
9
+ * Opens a JSON viewer dialog.
10
+ *
11
+ * @param data - The JSON data to display.
12
+ * @param options - Additional dialog options.
13
+ */
14
+ json(data?: any, options?: BaseDialogOptions): void;
15
+ }
16
+ }
17
+ //# sourceMappingURL=DialogService.d.ts.map
18
+ //#endregion
19
+ //#region ../../src/json/components/JsonViewer.d.ts
20
+ interface JsonViewerProps {
21
+ data: any;
22
+ /**
23
+ * Depth level to expand by default (0 = collapsed, Infinity = all expanded)
24
+ */
25
+ defaultExpandedDepth?: number;
26
+ /**
27
+ * Maximum nesting depth to render
28
+ */
29
+ maxDepth?: number;
30
+ /**
31
+ * Size variant
32
+ */
33
+ size?: MantineSize;
34
+ /**
35
+ * Whether to show quotes around keys and strings
36
+ */
37
+ showQuotes?: boolean;
38
+ /**
39
+ * Show copy button on row hover
40
+ */
41
+ showCopyButton?: boolean;
42
+ /**
43
+ * Custom value formatter. Return formatted string or undefined to use default.
44
+ */
45
+ formatValue?: (key: string | undefined, value: any, path: string[]) => string | number | undefined;
46
+ }
47
+ declare const JsonViewer: ({
48
+ data,
49
+ defaultExpandedDepth,
50
+ maxDepth,
51
+ size,
52
+ showQuotes,
53
+ showCopyButton,
54
+ formatValue
55
+ }: JsonViewerProps) => react_jsx_runtime0.JSX.Element;
56
+ //#endregion
57
+ export { JsonViewer };
58
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/json/extensions/DialogService.tsx","../../src/json/components/JsonViewer.tsx"],"sourcesContent":[],"mappings":";;;;;;;;;AAAuE;;;;IAYvB,IAAA,CAAA,IAAA,CAAA,EAAA,GAAA,EAAA,OAAA,CAAA,EAAjB,iBAAiB,CAAA,EAAA,IAAA;;;;;;UCetC,eAAA;;;;AD3B6D;EAAA,oBAAA,CAAA,EAAA,MAAA;;;;;;;ACQhD;EA4VV,IAAA,CAAA,EA5TJ,WAmeR;EAvK0B;;;EAAA,UAAA,CAAA,EAAA,OAAA;EAAA;;;EAQxB,cAAA,CAAA,EAAA,OAAA;EAAe;;;;;cARL;;;;;;;;GAQV,oBAAe,kBAAA,CAAA,GAAA,CAAA"}
@@ -0,0 +1,325 @@
1
+ import { DialogService, ui } from "@alepha/ui";
2
+ import { ActionIcon, Flex, Group, Text, Tree, getTreeExpandedState, useTree } from "@mantine/core";
3
+ import { IconCheck, IconChevronDown, IconChevronRight, IconCopy } from "@tabler/icons-react";
4
+ import { useCallback, useMemo, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region ../../src/json/components/JsonViewer.tsx
8
+ const SIZE_CONFIG = {
9
+ xs: {
10
+ icon: 14,
11
+ levelOffset: 16
12
+ },
13
+ sm: {
14
+ icon: 16,
15
+ levelOffset: 20
16
+ },
17
+ md: {
18
+ icon: 18,
19
+ levelOffset: 24
20
+ },
21
+ lg: {
22
+ icon: 20,
23
+ levelOffset: 28
24
+ },
25
+ xl: {
26
+ icon: 22,
27
+ levelOffset: 32
28
+ }
29
+ };
30
+ const STYLES = {
31
+ root: { fontFamily: "var(--mantine-font-family-monospace)" },
32
+ chevron: {
33
+ flexShrink: 0,
34
+ color: "var(--mantine-color-dimmed)"
35
+ },
36
+ key: {
37
+ color: "var(--mantine-color-cyan-text)",
38
+ fontWeight: 500
39
+ },
40
+ colon: { color: "var(--mantine-color-dimmed)" },
41
+ string: { color: "var(--mantine-color-teal-text)" },
42
+ number: { color: "var(--mantine-color-blue-text)" },
43
+ boolean: { color: "var(--mantine-color-violet-text)" },
44
+ null: {
45
+ color: "var(--mantine-color-dimmed)",
46
+ fontStyle: "italic"
47
+ },
48
+ preview: { color: "var(--mantine-color-dimmed)" }
49
+ };
50
+ const getValueType = (val) => {
51
+ if (val === null) return "null";
52
+ if (val === void 0) return "undefined";
53
+ if (Array.isArray(val)) return "array";
54
+ return typeof val;
55
+ };
56
+ function buildTreeNodes(data, path = [], key, isArrayItem = false, maxDepth = 10) {
57
+ const currentPath = key !== void 0 ? [...path, key] : path;
58
+ const nodeId = currentPath.length > 0 ? currentPath.join(".") : "root";
59
+ if (currentPath.length > maxDepth) return {
60
+ value: nodeId,
61
+ label: key ?? "",
62
+ nodeValue: data,
63
+ nodeKey: key,
64
+ path: currentPath,
65
+ isArrayItem
66
+ };
67
+ const type = getValueType(data);
68
+ if (type === "object" || type === "array") {
69
+ const children = (type === "array" ? data.map((v, i) => [String(i), v]) : Object.entries(data)).map(([k, v]) => buildTreeNodes(v, currentPath, k, type === "array", maxDepth)).filter((n) => n !== null);
70
+ return {
71
+ value: nodeId,
72
+ label: key ?? "",
73
+ nodeValue: data,
74
+ nodeKey: key,
75
+ path: currentPath,
76
+ isArrayItem,
77
+ children: children.length > 0 ? children : void 0
78
+ };
79
+ }
80
+ return {
81
+ value: nodeId,
82
+ label: key ?? "",
83
+ nodeValue: data,
84
+ nodeKey: key,
85
+ path: currentPath,
86
+ isArrayItem
87
+ };
88
+ }
89
+ function getExpandedIds(nodes, targetDepth, currentDepth = 0) {
90
+ if (currentDepth >= targetDepth) return [];
91
+ const ids = [];
92
+ for (const node of nodes) if (node.children) {
93
+ ids.push(node.value);
94
+ ids.push(...getExpandedIds(node.children, targetDepth, currentDepth + 1));
95
+ }
96
+ return ids;
97
+ }
98
+ const CopyButton = ({ value, iconSize }) => {
99
+ const [copied, setCopied] = useState(false);
100
+ const handleCopy = useCallback((e) => {
101
+ e.stopPropagation();
102
+ navigator.clipboard.writeText(value);
103
+ setCopied(true);
104
+ setTimeout(() => setCopied(false), 1500);
105
+ }, [value]);
106
+ return /* @__PURE__ */ jsx(ActionIcon, {
107
+ size: iconSize + 4,
108
+ variant: "transparent",
109
+ c: copied ? "green" : "dimmed",
110
+ onClick: handleCopy,
111
+ className: "alepha-json-viewer-copy",
112
+ children: copied ? /* @__PURE__ */ jsx(IconCheck, { size: iconSize }) : /* @__PURE__ */ jsx(IconCopy, { size: iconSize })
113
+ });
114
+ };
115
+ const RowNode = ({ node, expanded, hasChildren, elementProps, size, config, showQuotes, showCopyButton, renderValue }) => {
116
+ const { nodeValue, nodeKey, path, isArrayItem, isRoot } = node;
117
+ const type = getValueType(nodeValue);
118
+ const isExpandable = type === "object" || type === "array";
119
+ const getPreview = () => {
120
+ if (!isExpandable) return null;
121
+ const count = (type === "array" ? nodeValue : Object.keys(nodeValue)).length;
122
+ const label = type === "array" ? "item" : "key";
123
+ if (!expanded) return /* @__PURE__ */ jsx(Text, {
124
+ fs: "italic",
125
+ component: "span",
126
+ size,
127
+ style: STYLES.preview,
128
+ children: count === 0 ? type === "array" ? "[]" : "{}" : type === "array" ? `[ ${count} ${count === 1 ? label : `${label}s`} ]` : `{ ${count} ${count === 1 ? label : `${label}s`} }`
129
+ });
130
+ return null;
131
+ };
132
+ const getCopyValue = () => isExpandable ? JSON.stringify(nodeValue, null, 2) : String(nodeValue ?? "");
133
+ return /* @__PURE__ */ jsxs(Group, {
134
+ gap: 6,
135
+ wrap: "nowrap",
136
+ ...elementProps,
137
+ className: `alepha-json-viewer-row ${elementProps.className || ""}`,
138
+ children: [
139
+ hasChildren ? expanded ? /* @__PURE__ */ jsx(IconChevronDown, {
140
+ size: config.icon,
141
+ style: STYLES.chevron
142
+ }) : /* @__PURE__ */ jsx(IconChevronRight, {
143
+ size: config.icon,
144
+ style: STYLES.chevron
145
+ }) : /* @__PURE__ */ jsx("span", { style: {
146
+ width: config.icon,
147
+ flexShrink: 0
148
+ } }),
149
+ nodeKey !== void 0 && !isArrayItem && /* @__PURE__ */ jsxs(Text, {
150
+ component: "span",
151
+ size,
152
+ children: [/* @__PURE__ */ jsx("span", {
153
+ style: STYLES.key,
154
+ children: showQuotes ? `"${nodeKey}"` : nodeKey
155
+ }), /* @__PURE__ */ jsx("span", {
156
+ style: STYLES.colon,
157
+ children: ":"
158
+ })]
159
+ }),
160
+ nodeKey !== void 0 && isArrayItem && /* @__PURE__ */ jsxs(Text, {
161
+ component: "span",
162
+ size,
163
+ children: [/* @__PURE__ */ jsx("span", {
164
+ style: STYLES.key,
165
+ children: nodeKey
166
+ }), /* @__PURE__ */ jsx("span", {
167
+ style: STYLES.colon,
168
+ children: ":"
169
+ })]
170
+ }),
171
+ hasChildren ? getPreview() : isExpandable ? type === "array" ? /* @__PURE__ */ jsx(Text, {
172
+ component: "span",
173
+ size,
174
+ style: STYLES.preview,
175
+ children: "[]"
176
+ }) : /* @__PURE__ */ jsx(Text, {
177
+ component: "span",
178
+ size,
179
+ style: STYLES.preview,
180
+ children: "{}"
181
+ }) : renderValue(nodeValue, nodeKey, path),
182
+ showCopyButton && /* @__PURE__ */ jsx(CopyButton, {
183
+ value: getCopyValue(),
184
+ iconSize: config.icon
185
+ })
186
+ ]
187
+ });
188
+ };
189
+ const JsonViewer = ({ data, defaultExpandedDepth = 2, maxDepth = 10, size = "sm", showQuotes = false, showCopyButton = true, formatValue }) => {
190
+ const config = SIZE_CONFIG[size] || SIZE_CONFIG.sm;
191
+ const treeData = useMemo(() => {
192
+ const type = getValueType(data);
193
+ if (type === "object" || type === "array") {
194
+ const children = (type === "array" ? data.map((v, i) => [String(i), v]) : Object.entries(data)).map(([k, v]) => buildTreeNodes(v, [], k, type === "array", maxDepth)).filter((n) => n !== null);
195
+ return [{
196
+ value: "root",
197
+ label: "",
198
+ nodeValue: data,
199
+ nodeKey: void 0,
200
+ path: [],
201
+ isArrayItem: false,
202
+ isRoot: true,
203
+ children: children.length > 0 ? children : void 0
204
+ }];
205
+ }
206
+ const node = buildTreeNodes(data, [], void 0, false, maxDepth);
207
+ return node ? [node] : [];
208
+ }, [data, maxDepth]);
209
+ const tree = useTree({ initialExpandedState: useMemo(() => {
210
+ if (defaultExpandedDepth === 0) return {};
211
+ if (defaultExpandedDepth === Infinity) return getTreeExpandedState(treeData, "*");
212
+ return getTreeExpandedState(treeData, getExpandedIds(treeData, defaultExpandedDepth + 1));
213
+ }, [treeData, defaultExpandedDepth]) });
214
+ const renderValue = useCallback((val, key, path) => {
215
+ const custom = formatValue?.(key, val, path);
216
+ if (custom !== void 0) return /* @__PURE__ */ jsx(Text, {
217
+ component: "span",
218
+ size,
219
+ style: STYLES.string,
220
+ className: "alepha-json-viewer-value",
221
+ title: String(val),
222
+ children: custom
223
+ });
224
+ const type = getValueType(val);
225
+ switch (type) {
226
+ case "string": return /* @__PURE__ */ jsxs(Text, {
227
+ style: STYLES.string,
228
+ component: "span",
229
+ size,
230
+ className: "alepha-json-viewer-value",
231
+ title: val,
232
+ children: [
233
+ "\"",
234
+ val,
235
+ "\""
236
+ ]
237
+ });
238
+ case "number": return /* @__PURE__ */ jsx(Text, {
239
+ component: "span",
240
+ size,
241
+ style: STYLES.number,
242
+ children: val
243
+ });
244
+ case "boolean": return /* @__PURE__ */ jsx(Text, {
245
+ component: "span",
246
+ size,
247
+ style: STYLES.boolean,
248
+ children: String(val)
249
+ });
250
+ case "null":
251
+ case "undefined": return /* @__PURE__ */ jsx(Text, {
252
+ component: "span",
253
+ size,
254
+ style: STYLES.null,
255
+ children: type
256
+ });
257
+ default: return /* @__PURE__ */ jsx(Text, {
258
+ component: "span",
259
+ size,
260
+ children: String(val)
261
+ });
262
+ }
263
+ }, [
264
+ formatValue,
265
+ showQuotes,
266
+ size
267
+ ]);
268
+ const renderNode = useCallback(({ node, expanded, hasChildren, elementProps }) => {
269
+ return /* @__PURE__ */ jsx(RowNode, {
270
+ node,
271
+ expanded,
272
+ hasChildren,
273
+ elementProps,
274
+ size,
275
+ config,
276
+ showQuotes,
277
+ showCopyButton,
278
+ renderValue
279
+ });
280
+ }, [
281
+ config,
282
+ renderValue,
283
+ showCopyButton,
284
+ showQuotes,
285
+ size
286
+ ]);
287
+ if (treeData.length === 0) return /* @__PURE__ */ jsx(Text, {
288
+ size,
289
+ style: STYLES.null,
290
+ children: data === null ? "null" : data === void 0 ? "undefined" : "{}"
291
+ });
292
+ return /* @__PURE__ */ jsx(Tree, {
293
+ data: treeData,
294
+ tree,
295
+ levelOffset: config.levelOffset,
296
+ expandOnClick: true,
297
+ renderNode,
298
+ styles: { root: STYLES.root }
299
+ });
300
+ };
301
+
302
+ //#endregion
303
+ //#region ../../src/json/extensions/DialogService.tsx
304
+ DialogService.prototype.json = function(data, options) {
305
+ this.open({
306
+ size: "lg",
307
+ title: options?.title || "Json Viewer",
308
+ ...options,
309
+ content: /* @__PURE__ */ jsx(Flex, {
310
+ bdrs: "md",
311
+ w: "100%",
312
+ flex: 1,
313
+ p: "sm",
314
+ bg: ui.colors.surface,
315
+ children: /* @__PURE__ */ jsx(JsonViewer, {
316
+ size: "xs",
317
+ data
318
+ })
319
+ })
320
+ });
321
+ };
322
+
323
+ //#endregion
324
+ export { JsonViewer };
325
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/json/components/JsonViewer.tsx","../../src/json/extensions/DialogService.tsx"],"sourcesContent":["import {\n ActionIcon,\n Group,\n getTreeExpandedState,\n type MantineSize,\n Text,\n Tree,\n useTree,\n} from \"@mantine/core\";\nimport {\n IconCheck,\n IconChevronDown,\n IconChevronRight,\n IconCopy,\n} from \"@tabler/icons-react\";\nimport {\n type CSSProperties,\n type ReactNode,\n useCallback,\n useMemo,\n useState,\n} from \"react\";\n\n// =============================================================================\n// TYPES\n// =============================================================================\n\ninterface JsonViewerProps {\n data: any;\n /**\n * Depth level to expand by default (0 = collapsed, Infinity = all expanded)\n */\n defaultExpandedDepth?: number;\n /**\n * Maximum nesting depth to render\n */\n maxDepth?: number;\n /**\n * Size variant\n */\n size?: MantineSize;\n /**\n * Whether to show quotes around keys and strings\n */\n showQuotes?: boolean;\n /**\n * Show copy button on row hover\n */\n showCopyButton?: boolean;\n /**\n * Custom value formatter. Return formatted string or undefined to use default.\n */\n formatValue?: (\n key: string | undefined,\n value: any,\n path: string[],\n ) => string | number | undefined;\n}\n\ninterface JsonTreeNode {\n value: string;\n label: string;\n children?: JsonTreeNode[];\n // Custom properties\n nodeValue: any;\n nodeKey: string | undefined;\n path: string[];\n isArrayItem: boolean;\n isRoot?: boolean;\n}\n\n// =============================================================================\n// CONSTANTS\n// =============================================================================\n\nconst SIZE_CONFIG: Record<MantineSize, { icon: number; levelOffset: number }> =\n {\n xs: { icon: 14, levelOffset: 16 },\n sm: { icon: 16, levelOffset: 20 },\n md: { icon: 18, levelOffset: 24 },\n lg: { icon: 20, levelOffset: 28 },\n xl: { icon: 22, levelOffset: 32 },\n };\n\nconst STYLES = {\n root: {\n fontFamily: \"var(--mantine-font-family-monospace)\",\n } satisfies CSSProperties,\n chevron: {\n flexShrink: 0,\n color: \"var(--mantine-color-dimmed)\",\n } satisfies CSSProperties,\n key: {\n color: \"var(--mantine-color-cyan-text)\",\n fontWeight: 500,\n } satisfies CSSProperties,\n colon: {\n color: \"var(--mantine-color-dimmed)\",\n } satisfies CSSProperties,\n string: {\n color: \"var(--mantine-color-teal-text)\",\n } satisfies CSSProperties,\n number: {\n color: \"var(--mantine-color-blue-text)\",\n } satisfies CSSProperties,\n boolean: {\n color: \"var(--mantine-color-violet-text)\",\n } satisfies CSSProperties,\n null: {\n color: \"var(--mantine-color-dimmed)\",\n fontStyle: \"italic\",\n } satisfies CSSProperties,\n preview: {\n color: \"var(--mantine-color-dimmed)\",\n } satisfies CSSProperties,\n};\n\n// =============================================================================\n// HELPERS\n// =============================================================================\n\nconst getValueType = (val: any): string => {\n if (val === null) return \"null\";\n if (val === undefined) return \"undefined\";\n if (Array.isArray(val)) return \"array\";\n return typeof val;\n};\n\n// Convert JSON to tree data structure\nfunction buildTreeNodes(\n data: any,\n path: string[] = [],\n key?: string,\n isArrayItem = false,\n maxDepth = 10,\n): JsonTreeNode | null {\n const currentPath = key !== undefined ? [...path, key] : path;\n const nodeId = currentPath.length > 0 ? currentPath.join(\".\") : \"root\";\n\n if (currentPath.length > maxDepth) {\n return {\n value: nodeId,\n label: key ?? \"\",\n nodeValue: data,\n nodeKey: key,\n path: currentPath,\n isArrayItem,\n };\n }\n\n const type = getValueType(data);\n\n if (type === \"object\" || type === \"array\") {\n const entries =\n type === \"array\"\n ? (data as any[]).map((v, i) => [String(i), v] as const)\n : Object.entries(data);\n\n const children = entries\n .map(([k, v]) =>\n buildTreeNodes(v, currentPath, k, type === \"array\", maxDepth),\n )\n .filter((n): n is JsonTreeNode => n !== null);\n\n return {\n value: nodeId,\n label: key ?? \"\",\n nodeValue: data,\n nodeKey: key,\n path: currentPath,\n isArrayItem,\n children: children.length > 0 ? children : undefined,\n };\n }\n\n return {\n value: nodeId,\n label: key ?? \"\",\n nodeValue: data,\n nodeKey: key,\n path: currentPath,\n isArrayItem,\n };\n}\n\n// Get all expandable node IDs up to a certain depth\nfunction getExpandedIds(\n nodes: JsonTreeNode[],\n targetDepth: number,\n currentDepth = 0,\n): string[] {\n if (currentDepth >= targetDepth) return [];\n const ids: string[] = [];\n for (const node of nodes) {\n if (node.children) {\n ids.push(node.value);\n ids.push(...getExpandedIds(node.children, targetDepth, currentDepth + 1));\n }\n }\n return ids;\n}\n\n// =============================================================================\n// COPY BUTTON COMPONENT\n// =============================================================================\n\nconst CopyButton = ({\n value,\n iconSize,\n}: {\n value: string;\n iconSize: number;\n}) => {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n navigator.clipboard.writeText(value);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n },\n [value],\n );\n\n return (\n <ActionIcon\n size={iconSize + 4}\n variant=\"transparent\"\n c={copied ? \"green\" : \"dimmed\"}\n onClick={handleCopy}\n className=\"alepha-json-viewer-copy\"\n >\n {copied ? <IconCheck size={iconSize} /> : <IconCopy size={iconSize} />}\n </ActionIcon>\n );\n};\n\n// =============================================================================\n// ROW NODE COMPONENT\n// =============================================================================\n\ninterface RowNodeProps {\n node: JsonTreeNode;\n expanded: boolean;\n hasChildren: boolean;\n elementProps: any;\n size: MantineSize;\n config: { icon: number; levelOffset: number };\n showQuotes: boolean;\n showCopyButton: boolean;\n renderValue: (val: any, key: string | undefined, path: string[]) => ReactNode;\n}\n\nconst RowNode = ({\n node,\n expanded,\n hasChildren,\n elementProps,\n size,\n config,\n showQuotes,\n showCopyButton,\n renderValue,\n}: RowNodeProps) => {\n const { nodeValue, nodeKey, path, isArrayItem, isRoot } = node;\n const type = getValueType(nodeValue);\n const isExpandable = type === \"object\" || type === \"array\";\n\n const getPreview = () => {\n if (!isExpandable) return null;\n const entries = type === \"array\" ? nodeValue : Object.keys(nodeValue);\n const count = entries.length;\n const label = type === \"array\" ? \"item\" : \"key\";\n\n // For root node or collapsed nodes, show the count\n if (!expanded) {\n return (\n <Text fs={\"italic\"} component=\"span\" size={size} style={STYLES.preview}>\n {count === 0\n ? type === \"array\"\n ? \"[]\"\n : \"{}\"\n : type === \"array\"\n ? `[ ${count} ${count === 1 ? label : `${label}s`} ]`\n : `{ ${count} ${count === 1 ? label : `${label}s`} }`}\n </Text>\n );\n }\n\n return null;\n };\n\n const getCopyValue = () =>\n isExpandable ? JSON.stringify(nodeValue, null, 2) : String(nodeValue ?? \"\");\n\n return (\n <Group\n gap={6}\n wrap=\"nowrap\"\n {...elementProps}\n className={`alepha-json-viewer-row ${elementProps.className || \"\"}`}\n >\n {hasChildren ? (\n expanded ? (\n <IconChevronDown size={config.icon} style={STYLES.chevron} />\n ) : (\n <IconChevronRight size={config.icon} style={STYLES.chevron} />\n )\n ) : (\n <span style={{ width: config.icon, flexShrink: 0 }} />\n )}\n\n {nodeKey !== undefined && !isArrayItem && (\n <Text component=\"span\" size={size}>\n <span style={STYLES.key}>\n {showQuotes ? `\"${nodeKey}\"` : nodeKey}\n </span>\n <span style={STYLES.colon}>:</span>\n </Text>\n )}\n\n {nodeKey !== undefined && isArrayItem && (\n <Text component=\"span\" size={size}>\n <span style={STYLES.key}>{nodeKey}</span>\n <span style={STYLES.colon}>:</span>\n </Text>\n )}\n\n {hasChildren ? (\n getPreview()\n ) : isExpandable ? (\n type === \"array\" ? (\n <Text component=\"span\" size={size} style={STYLES.preview}>\n []\n </Text>\n ) : (\n <Text component=\"span\" size={size} style={STYLES.preview}>\n {\"{}\"}\n </Text>\n )\n ) : (\n renderValue(nodeValue, nodeKey, path)\n )}\n\n {showCopyButton && (\n <CopyButton value={getCopyValue()} iconSize={config.icon} />\n )}\n </Group>\n );\n};\n\n// =============================================================================\n// MAIN COMPONENT\n// =============================================================================\n\nexport const JsonViewer = ({\n data,\n defaultExpandedDepth = 2,\n maxDepth = 10,\n size = \"sm\",\n showQuotes = false,\n showCopyButton = true,\n formatValue,\n}: JsonViewerProps) => {\n const config = SIZE_CONFIG[size] || SIZE_CONFIG.sm;\n\n // Build tree data from JSON with root wrapper\n const treeData = useMemo(() => {\n const type = getValueType(data);\n\n // For objects and arrays, create a root node wrapper\n if (type === \"object\" || type === \"array\") {\n const entries =\n type === \"array\"\n ? (data as any[]).map((v, i) => [String(i), v] as const)\n : Object.entries(data);\n const children = entries\n .map(([k, v]) => buildTreeNodes(v, [], k, type === \"array\", maxDepth))\n .filter((n): n is JsonTreeNode => n !== null);\n\n const rootNode: JsonTreeNode = {\n value: \"root\",\n label: \"\",\n nodeValue: data,\n nodeKey: undefined,\n path: [],\n isArrayItem: false,\n isRoot: true,\n children: children.length > 0 ? children : undefined,\n };\n return [rootNode];\n }\n\n // For primitives, just show the value directly\n const node = buildTreeNodes(data, [], undefined, false, maxDepth);\n return node ? [node] : [];\n }, [data, maxDepth]);\n\n // Compute initial expanded state (root is always expanded by default unless depth is 0)\n const initialExpandedState = useMemo(() => {\n if (defaultExpandedDepth === 0) return {};\n if (defaultExpandedDepth === Infinity) {\n return getTreeExpandedState(treeData, \"*\");\n }\n // Add 1 to depth to account for root node\n const ids = getExpandedIds(treeData, defaultExpandedDepth + 1);\n return getTreeExpandedState(treeData, ids);\n }, [treeData, defaultExpandedDepth]);\n\n const tree = useTree({ initialExpandedState });\n\n // Render value based on type\n const renderValue = useCallback(\n (val: any, key: string | undefined, path: string[]): ReactNode => {\n const custom = formatValue?.(key, val, path);\n if (custom !== undefined) {\n return (\n <Text\n component=\"span\"\n size={size}\n style={STYLES.string}\n className=\"alepha-json-viewer-value\"\n title={String(val)}\n >\n {custom}\n </Text>\n );\n }\n\n const type = getValueType(val);\n switch (type) {\n case \"string\": {\n return (\n <Text\n style={STYLES.string}\n component=\"span\"\n size={size}\n className=\"alepha-json-viewer-value\"\n title={val}\n >\n \"{val}\"\n </Text>\n );\n }\n case \"number\":\n return (\n <Text component=\"span\" size={size} style={STYLES.number}>\n {val}\n </Text>\n );\n case \"boolean\":\n return (\n <Text component=\"span\" size={size} style={STYLES.boolean}>\n {String(val)}\n </Text>\n );\n case \"null\":\n case \"undefined\":\n return (\n <Text component=\"span\" size={size} style={STYLES.null}>\n {type}\n </Text>\n );\n default:\n return (\n <Text component=\"span\" size={size}>\n {String(val)}\n </Text>\n );\n }\n },\n [formatValue, showQuotes, size],\n );\n\n // Render tree node\n const renderNode = useCallback(\n ({\n node,\n expanded,\n hasChildren,\n elementProps,\n }: {\n node: JsonTreeNode;\n expanded: boolean;\n hasChildren: boolean;\n elementProps: any;\n }): ReactNode => {\n return (\n <RowNode\n node={node}\n expanded={expanded}\n hasChildren={hasChildren}\n elementProps={elementProps}\n size={size}\n config={config}\n showQuotes={showQuotes}\n showCopyButton={showCopyButton}\n renderValue={renderValue}\n />\n );\n },\n [config, renderValue, showCopyButton, showQuotes, size],\n );\n\n if (treeData.length === 0) {\n return (\n <Text size={size} style={STYLES.null}>\n {data === null ? \"null\" : data === undefined ? \"undefined\" : \"{}\"}\n </Text>\n );\n }\n\n return (\n <Tree\n data={treeData}\n tree={tree}\n levelOffset={config.levelOffset}\n expandOnClick\n renderNode={renderNode as any}\n styles={{ root: STYLES.root }}\n />\n );\n};\n\nexport default JsonViewer;\n","import { type BaseDialogOptions, DialogService, ui } from \"@alepha/ui\";\nimport { Flex } from \"@mantine/core\";\nimport { JsonViewer } from \"../components/JsonViewer.tsx\";\n\ndeclare module \"@alepha/ui\" {\n interface DialogService {\n /**\n * Opens a JSON viewer dialog.\n *\n * @param data - The JSON data to display.\n * @param options - Additional dialog options.\n */\n json(data?: any, options?: BaseDialogOptions): void;\n }\n}\n\nDialogService.prototype.json = function (\n data?: any,\n options?: BaseDialogOptions,\n) {\n this.open({\n size: \"lg\",\n title: options?.title || \"Json Viewer\",\n ...options,\n content: (\n <Flex bdrs={\"md\"} w={\"100%\"} flex={1} p={\"sm\"} bg={ui.colors.surface}>\n <JsonViewer size={\"xs\"} data={data} />\n </Flex>\n ),\n });\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "mantine"
8
8
  ],
9
9
  "author": "Nicolas Foures",
10
- "version": "0.14.1",
10
+ "version": "0.14.3",
11
11
  "type": "module",
12
12
  "engines": {
13
13
  "node": ">=22.0.0"
@@ -22,24 +22,24 @@
22
22
  "styles.css"
23
23
  ],
24
24
  "dependencies": {
25
- "@alepha/react": "0.14.1",
26
- "@mantine/core": "^8.3.10",
27
- "@mantine/dates": "^8.3.10",
28
- "@mantine/hooks": "^8.3.10",
29
- "@mantine/modals": "^8.3.10",
30
- "@mantine/notifications": "^8.3.10",
31
- "@mantine/nprogress": "^8.3.10",
32
- "@mantine/spotlight": "^8.3.10",
33
- "@tabler/icons-react": "^3.36.0",
34
- "alepha": "0.14.1",
25
+ "@alepha/react": "0.14.3",
26
+ "@mantine/core": "^8.3.11",
27
+ "@mantine/dates": "^8.3.11",
28
+ "@mantine/hooks": "^8.3.11",
29
+ "@mantine/modals": "^8.3.11",
30
+ "@mantine/notifications": "^8.3.11",
31
+ "@mantine/nprogress": "^8.3.11",
32
+ "@mantine/spotlight": "^8.3.11",
33
+ "@tabler/icons-react": "^3.36.1",
34
+ "alepha": "0.14.3",
35
35
  "dayjs": "^1.11.19"
36
36
  },
37
37
  "devDependencies": {
38
- "@biomejs/biome": "^2.3.10",
38
+ "@biomejs/biome": "^2.3.11",
39
39
  "react": "^19.2.3",
40
40
  "react-dom": "^19.2.3",
41
41
  "typescript": "^5.9.3",
42
- "vite": "^7.3.0",
42
+ "vite": "^7.3.1",
43
43
  "vitest": "^4.0.16"
44
44
  },
45
45
  "peerDependencies": {
@@ -74,6 +74,17 @@
74
74
  "import": "./dist/core/index.js",
75
75
  "default": "./dist/core/index.js"
76
76
  },
77
- "./styles": "./styles.css"
77
+ "./demo": {
78
+ "types": "./dist/demo/index.d.ts",
79
+ "import": "./dist/demo/index.js",
80
+ "default": "./dist/demo/index.js"
81
+ },
82
+ "./json": {
83
+ "types": "./dist/json/index.d.ts",
84
+ "import": "./dist/json/index.js",
85
+ "default": "./dist/json/index.js"
86
+ },
87
+ "./styles": "./src/core/styles.css",
88
+ "./json/styles": "./src/json/styles.css"
78
89
  }
79
90
  }
@@ -1,5 +1,5 @@
1
- import { $page, ReactRouter, Redirection } from "@alepha/react";
2
1
  import { ReactAuth } from "@alepha/react/auth";
2
+ import { $page, ReactRouter, Redirection } from "@alepha/react/router";
3
3
  import type { AdminShellProps } from "@alepha/ui";
4
4
  import { AuthRouter } from "@alepha/ui/auth";
5
5
  import {
@@ -13,23 +13,26 @@ import {
13
13
  IconUsers,
14
14
  } from "@tabler/icons-react";
15
15
  import { $inject } from "alepha";
16
- import type { AuditController } from "alepha/api/audits";
16
+ import type { AdminAuditController } from "alepha/api/audits";
17
17
  import type { FileController } from "alepha/api/files";
18
- import type { NotificationController } from "alepha/api/notifications";
19
- import type { ConfigController } from "alepha/api/parameters";
20
- import type { SessionController, UserController } from "alepha/api/users";
18
+ import type { AdminNotificationController } from "alepha/api/notifications";
19
+ import type { AdminConfigController } from "alepha/api/parameters";
20
+ import type {
21
+ AdminSessionController,
22
+ AdminUserController,
23
+ } from "alepha/api/users";
21
24
  import { $client } from "alepha/server/links";
22
25
 
23
26
  export class AdminRouter {
24
27
  protected readonly router = $inject(ReactRouter);
25
28
  protected readonly authRouter = $inject(AuthRouter);
26
29
  protected readonly auth = $inject(ReactAuth);
27
- protected readonly userCtrl = $client<UserController>();
28
- protected readonly sessionCtrl = $client<SessionController>();
29
- protected readonly notificationCtrl = $client<NotificationController>();
30
+ protected readonly userCtrl = $client<AdminUserController>();
31
+ protected readonly sessionCtrl = $client<AdminSessionController>();
32
+ protected readonly notificationCtrl = $client<AdminNotificationController>();
30
33
  protected readonly fileCtrl = $client<FileController>();
31
- protected readonly configCtrl = $client<ConfigController>();
32
- protected readonly auditCtrl = $client<AuditController>();
34
+ protected readonly configCtrl = $client<AdminConfigController>();
35
+ protected readonly auditCtrl = $client<AdminAuditController>();
33
36
 
34
37
  protected adminShellProps(): AdminShellProps {
35
38
  return {};
@@ -49,10 +52,10 @@ export class AdminRouter {
49
52
  // Layout
50
53
  // ─────────────────────────────────────────────────────────────────────────────
51
54
 
52
- public readonly layout = $page({
53
- name: "AdminLayout",
55
+ public readonly adminLayout = $page({
54
56
  path: "/admin",
55
57
  label: "Admin",
58
+ can: () => this.userCtrl.findUsers.can(),
56
59
  lazy: () => import("./components/AdminLayout.tsx"),
57
60
  props: () => ({
58
61
  adminShellProps: this.adminShellProps(),
@@ -71,7 +74,7 @@ export class AdminRouter {
71
74
 
72
75
  public readonly adminUsers = $page({
73
76
  icon: IconUsers,
74
- parent: this.layout,
77
+ parent: this.adminLayout,
75
78
  path: "/users",
76
79
  label: "Users",
77
80
  description: "Manage application users and their roles.",
@@ -81,7 +84,7 @@ export class AdminRouter {
81
84
 
82
85
  public readonly adminUserCreate = $page({
83
86
  icon: IconPlus,
84
- parent: this.layout,
87
+ parent: this.adminLayout,
85
88
  path: "/users/create",
86
89
  label: "Create User",
87
90
  description: "Create a new user account.",
@@ -91,7 +94,7 @@ export class AdminRouter {
91
94
 
92
95
  public readonly adminUserLayout = $page({
93
96
  icon: IconUser,
94
- parent: this.layout,
97
+ parent: this.adminLayout,
95
98
  path: "/users/:userId",
96
99
  label: "User",
97
100
  lazy: () => import("./components/users/AdminUserLayout.tsx"),
@@ -133,7 +136,7 @@ export class AdminRouter {
133
136
 
134
137
  public readonly adminAudits = $page({
135
138
  icon: IconHistory,
136
- parent: this.layout,
139
+ parent: this.adminLayout,
137
140
  path: "/audits",
138
141
  label: "Audit Log",
139
142
  description: "View system-wide audit trail and activity logs.",
@@ -147,7 +150,7 @@ export class AdminRouter {
147
150
 
148
151
  public readonly adminSessions = $page({
149
152
  icon: IconDevices,
150
- parent: this.layout,
153
+ parent: this.adminLayout,
151
154
  path: "/sessions",
152
155
  label: "Sessions",
153
156
  description: "View and manage all active sessions.",
@@ -161,7 +164,7 @@ export class AdminRouter {
161
164
 
162
165
  public readonly adminNotifications = $page({
163
166
  icon: IconBell,
164
- parent: this.layout,
167
+ parent: this.adminLayout,
165
168
  path: "/notifications",
166
169
  label: "Notifications",
167
170
  description: "View notification history and status.",
@@ -175,7 +178,7 @@ export class AdminRouter {
175
178
 
176
179
  public readonly adminFiles = $page({
177
180
  icon: IconFile,
178
- parent: this.layout,
181
+ parent: this.adminLayout,
179
182
  path: "/files",
180
183
  label: "Files",
181
184
  description: "Manage uploaded files and storage.",
@@ -189,7 +192,7 @@ export class AdminRouter {
189
192
 
190
193
  public readonly adminParameters = $page({
191
194
  icon: IconSettings,
192
- parent: this.layout,
195
+ parent: this.adminLayout,
193
196
  path: "/parameters",
194
197
  label: "Parameters",
195
198
  description: "View and manage application configuration parameters.",
@@ -1,4 +1,4 @@
1
- import { $page } from "@alepha/react";
1
+ import { $page } from "@alepha/react/router";
2
2
  import { AlephaMantineProvider } from "@alepha/ui";
3
3
  import { AuthRouter } from "@alepha/ui/auth";
4
4
  import { $inject } from "alepha";
@@ -18,6 +18,6 @@ export class MainRouter {
18
18
 
19
19
  layout = $page({
20
20
  component: AlephaMantineProvider,
21
- children: () => [this.auth.layout, this.admin.layout],
21
+ children: () => [this.auth.layout, this.admin.adminLayout],
22
22
  });
23
23
  }