@alepha/ui 0.18.3 → 0.19.1

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 (181) hide show
  1. package/dist/admin/{AdminApiKeys-Dy_k-4Vd.js → AdminApiKeys-C2ze85eD.js} +3 -4
  2. package/dist/admin/{AdminApiKeys-Dy_k-4Vd.js.map → AdminApiKeys-C2ze85eD.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-CKiFMSSU.js → AdminAudits-BIj81e4k.js} +3 -4
  4. package/dist/admin/{AdminAudits-CKiFMSSU.js.map → AdminAudits-BIj81e4k.js.map} +1 -1
  5. package/dist/admin/{AdminDashboard-PhC_dZqo.js → AdminDashboard-PMVzrwSu.js} +3 -4
  6. package/dist/admin/{AdminDashboard-PhC_dZqo.js.map → AdminDashboard-PMVzrwSu.js.map} +1 -1
  7. package/dist/admin/AdminFiles-Bq03BLt-.js +189 -0
  8. package/dist/admin/AdminFiles-Bq03BLt-.js.map +1 -0
  9. package/dist/admin/{AdminJobExecutions-D9E-CS-U.js → AdminJobs-D1_QGCDy.js} +401 -358
  10. package/dist/admin/AdminJobs-D1_QGCDy.js.map +1 -0
  11. package/dist/admin/{AdminLayout-I6TlUMPc.js → AdminLayout-BNiwiw2D.js} +8 -25
  12. package/dist/admin/AdminLayout-BNiwiw2D.js.map +1 -0
  13. package/dist/admin/{AdminNotifications-ZPHCYrv7.js → AdminNotifications-DSKQtUfn.js} +85 -124
  14. package/dist/admin/AdminNotifications-DSKQtUfn.js.map +1 -0
  15. package/dist/admin/{AdminParameters-CqgvhRsb.js → AdminParameters-CoB7EhyM.js} +3 -12
  16. package/dist/admin/{AdminParameters-CqgvhRsb.js.map → AdminParameters-CoB7EhyM.js.map} +1 -1
  17. package/dist/admin/{AdminSessions-Bz5NRuoW.js → AdminSessions-DFbFcrJQ.js} +3 -4
  18. package/dist/admin/{AdminSessions-Bz5NRuoW.js.map → AdminSessions-DFbFcrJQ.js.map} +1 -1
  19. package/dist/admin/{AdminUserLayout-lXT6I0Qq.js → AdminUserLayout-fSfi3KMm.js} +72 -111
  20. package/dist/admin/AdminUserLayout-fSfi3KMm.js.map +1 -0
  21. package/dist/admin/{AdminUserProfile-vFBLoJ3h.js → AdminUserProfile-_C-h8vUK.js} +7 -6
  22. package/dist/admin/AdminUserProfile-_C-h8vUK.js.map +1 -0
  23. package/dist/admin/{AdminUserSessions-CT_YDim0.js → AdminUserSessions-KpJHIeQo.js} +3 -4
  24. package/dist/admin/{AdminUserSessions-CT_YDim0.js.map → AdminUserSessions-KpJHIeQo.js.map} +1 -1
  25. package/dist/admin/{AdminUsers-D1UfGya9.js → AdminUsers-DcVrzdQP.js} +4 -4
  26. package/dist/admin/AdminUsers-DcVrzdQP.js.map +1 -0
  27. package/dist/admin/{AuthLayout-_frhdgOO.js → AuthLayout-CazfLzcf.js} +3 -4
  28. package/dist/admin/{AuthLayout-_frhdgOO.js.map → AuthLayout-CazfLzcf.js.map} +1 -1
  29. package/dist/{demo/IconGoogle-CSQLPYwX.js → admin/IconGoogle-8Nkx6yax.js} +2 -4
  30. package/dist/admin/{IconGoogle-Ch1m3Uzl.js.map → IconGoogle-8Nkx6yax.js.map} +1 -1
  31. package/dist/admin/{Login-xtNmQtGh.js → Login-CaMjUrDP.js} +5 -6
  32. package/dist/{auth/Login-BA1E8IZl.js.map → admin/Login-CaMjUrDP.js.map} +1 -1
  33. package/dist/admin/{Profile-_AtPUwAP.js → Profile-Ca4fZX15.js} +3 -5
  34. package/dist/{demo/Profile-DS5q4vOh.js.map → admin/Profile-Ca4fZX15.js.map} +1 -1
  35. package/dist/admin/{Register-JcCjHUUn.js → Register-C5DyKWPO.js} +5 -6
  36. package/dist/{demo/Register-B4hLBeEv.js.map → admin/Register-C5DyKWPO.js.map} +1 -1
  37. package/dist/admin/{ResetPassword-CwGBPLJO.js → ResetPassword-BA5sAgXo.js} +4 -5
  38. package/dist/{auth/ResetPassword-DCtGcneA.js.map → admin/ResetPassword-BA5sAgXo.js.map} +1 -1
  39. package/dist/admin/{VerifyEmail-hNxWejWf.js → VerifyEmail-DKNXROj_.js} +4 -5
  40. package/dist/{auth/VerifyEmail-DkH7NBfn.js.map → admin/VerifyEmail-DKNXROj_.js.map} +1 -1
  41. package/dist/admin/adminUserAtom-BLNc7XbT.js +11 -0
  42. package/dist/admin/adminUserAtom-BLNc7XbT.js.map +1 -0
  43. package/dist/admin/{core-CYaRQ8O-.js → core-CJCEx18C.js} +132 -86
  44. package/dist/admin/core-CJCEx18C.js.map +1 -0
  45. package/dist/admin/index.d.ts +80 -13
  46. package/dist/admin/index.d.ts.map +1 -1
  47. package/dist/admin/index.js +38 -68
  48. package/dist/admin/index.js.map +1 -1
  49. package/dist/admin/rolldown-runtime-CiIaOW0V.js +13 -0
  50. package/dist/{demo/AuthLayout-Brri4A-L.js → auth/AuthLayout-vXPcCVzp.js} +3 -4
  51. package/dist/auth/{AuthLayout-AvLlcLjS.js.map → AuthLayout-vXPcCVzp.js.map} +1 -1
  52. package/dist/{admin/IconGoogle-Ch1m3Uzl.js → auth/IconGoogle-8Nkx6yax.js} +2 -4
  53. package/dist/auth/{IconGoogle-Ch1m3Uzl.js.map → IconGoogle-8Nkx6yax.js.map} +1 -1
  54. package/dist/auth/{Login-BA1E8IZl.js → Login-Dg08QR20.js} +5 -6
  55. package/dist/{demo/Login-C12N4oGs.js.map → auth/Login-Dg08QR20.js.map} +1 -1
  56. package/dist/{demo/Profile-DS5q4vOh.js → auth/Profile-Bb5O1yeh.js} +3 -5
  57. package/dist/auth/{Profile-YcWdeuFz.js.map → Profile-Bb5O1yeh.js.map} +1 -1
  58. package/dist/auth/{Register-CPhEO5MG.js → Register-B2AN71NC.js} +5 -6
  59. package/dist/{admin/Register-JcCjHUUn.js.map → auth/Register-B2AN71NC.js.map} +1 -1
  60. package/dist/{demo/ResetPassword-D8g9ha1N.js → auth/ResetPassword-BLxwzbDj.js} +4 -5
  61. package/dist/{admin/ResetPassword-CwGBPLJO.js.map → auth/ResetPassword-BLxwzbDj.js.map} +1 -1
  62. package/dist/auth/{VerifyEmail-DkH7NBfn.js → VerifyEmail-CSDOk3Zm.js} +4 -5
  63. package/dist/{admin/VerifyEmail-hNxWejWf.js.map → auth/VerifyEmail-CSDOk3Zm.js.map} +1 -1
  64. package/dist/auth/{core-D5jIAVF2.js → core-DuGkjPiU.js} +23 -54
  65. package/dist/auth/core-DuGkjPiU.js.map +1 -0
  66. package/dist/auth/index.d.ts +20 -6
  67. package/dist/auth/index.d.ts.map +1 -1
  68. package/dist/auth/index.js +13 -18
  69. package/dist/auth/index.js.map +1 -1
  70. package/dist/auth/rolldown-runtime-CiIaOW0V.js +13 -0
  71. package/dist/core/index.d.ts +78 -20
  72. package/dist/core/index.d.ts.map +1 -1
  73. package/dist/core/index.js +130 -98
  74. package/dist/core/index.js.map +1 -1
  75. package/dist/{auth/AuthLayout-AvLlcLjS.js → demo/AuthLayout-DPsOOG4u.js} +3 -4
  76. package/dist/demo/{AuthLayout-Brri4A-L.js.map → AuthLayout-DPsOOG4u.js.map} +1 -1
  77. package/dist/demo/{DemoButton-wiCxZZ_L.js → DemoButton-wzcqGk4u.js} +4 -5
  78. package/dist/demo/{DemoButton-wiCxZZ_L.js.map → DemoButton-wzcqGk4u.js.map} +1 -1
  79. package/dist/demo/{DemoControlSelect-D7ILObVg.js → DemoControlSelect-CMWvQ6Gm.js} +4 -5
  80. package/dist/demo/{DemoControlSelect-D7ILObVg.js.map → DemoControlSelect-CMWvQ6Gm.js.map} +1 -1
  81. package/dist/demo/{DemoDataTable-DZ5Y8pFX.js → DemoDataTable-CHsAP3e2.js} +4 -5
  82. package/dist/demo/{DemoDataTable-DZ5Y8pFX.js.map → DemoDataTable-CHsAP3e2.js.map} +1 -1
  83. package/dist/demo/{DemoDialog-CUWdLHim.js → DemoDialog-Co2IePxX.js} +3 -4
  84. package/dist/demo/{DemoDialog-CUWdLHim.js.map → DemoDialog-Co2IePxX.js.map} +1 -1
  85. package/dist/demo/{DemoFlex-a8OhMMvq.js → DemoFlex-OEwQt5do.js} +4 -5
  86. package/dist/demo/{DemoFlex-a8OhMMvq.js.map → DemoFlex-OEwQt5do.js.map} +1 -1
  87. package/dist/demo/DemoHeading-Db-XkQIK.js +69 -0
  88. package/dist/demo/DemoHeading-Db-XkQIK.js.map +1 -0
  89. package/dist/demo/{DemoHome-D_De3UiT.js → DemoHome-Cyp29ygy.js} +4 -5
  90. package/dist/demo/{DemoHome-D_De3UiT.js.map → DemoHome-Cyp29ygy.js.map} +1 -1
  91. package/dist/demo/{DemoJsonViewer-B50s9aGM.js → DemoJsonViewer-DXtCeMzH.js} +4 -5
  92. package/dist/demo/{DemoJsonViewer-B50s9aGM.js.map → DemoJsonViewer-DXtCeMzH.js.map} +1 -1
  93. package/dist/demo/{DemoLayout-CHU8WTwO.js → DemoLayout-hh9VmZQP.js} +4 -5
  94. package/dist/demo/DemoLayout-hh9VmZQP.js.map +1 -0
  95. package/dist/demo/{DemoLogin-BBlrWpml.js → DemoLogin-DX7mnmkh.js} +15 -11
  96. package/dist/demo/{DemoLogin-BBlrWpml.js.map → DemoLogin-DX7mnmkh.js.map} +1 -1
  97. package/dist/demo/{DemoRegister-BuNE3_-f.js → DemoRegister-DVcZl04m.js} +15 -11
  98. package/dist/demo/{DemoRegister-BuNE3_-f.js.map → DemoRegister-DVcZl04m.js.map} +1 -1
  99. package/dist/demo/{DemoResetPassword-D_IjjjOJ.js → DemoResetPassword-CPENlZH5.js} +15 -11
  100. package/dist/demo/{DemoResetPassword-D_IjjjOJ.js.map → DemoResetPassword-CPENlZH5.js.map} +1 -1
  101. package/dist/demo/{DemoSidebar-Giy2HRBD.js → DemoSidebar-CGu7DZeM.js} +4 -5
  102. package/dist/demo/{DemoSidebar-Giy2HRBD.js.map → DemoSidebar-CGu7DZeM.js.map} +1 -1
  103. package/dist/demo/{DemoText-ubcw-vog.js → DemoText-DYUJ7bY_.js} +4 -5
  104. package/dist/demo/{DemoText-ubcw-vog.js.map → DemoText-DYUJ7bY_.js.map} +1 -1
  105. package/dist/demo/{DemoToast-9die_dYT.js → DemoToast-CgdnZNvx.js} +3 -4
  106. package/dist/demo/{DemoToast-9die_dYT.js.map → DemoToast-CgdnZNvx.js.map} +1 -1
  107. package/dist/demo/{DemoTypeForm-D_d6OVKL.js → DemoTypeForm-Pims-cGa.js} +4 -5
  108. package/dist/demo/{DemoTypeForm-D_d6OVKL.js.map → DemoTypeForm-Pims-cGa.js.map} +1 -1
  109. package/dist/demo/{DemoVerifyEmail-B43KlF4F.js → DemoVerifyEmail-C7B3xxch.js} +10 -11
  110. package/dist/demo/{DemoVerifyEmail-B43KlF4F.js.map → DemoVerifyEmail-C7B3xxch.js.map} +1 -1
  111. package/dist/{auth/IconGoogle-Ch1m3Uzl.js → demo/IconGoogle-CwQy4G9y.js} +2 -4
  112. package/dist/demo/{IconGoogle-CSQLPYwX.js.map → IconGoogle-CwQy4G9y.js.map} +1 -1
  113. package/dist/demo/{Login-C12N4oGs.js → Login-pwMF4TUj.js} +5 -6
  114. package/dist/{admin/Login-xtNmQtGh.js.map → demo/Login-pwMF4TUj.js.map} +1 -1
  115. package/dist/{auth/Profile-YcWdeuFz.js → demo/Profile-BliZapZS.js} +3 -5
  116. package/dist/{admin/Profile-_AtPUwAP.js.map → demo/Profile-BliZapZS.js.map} +1 -1
  117. package/dist/demo/{Register-B4hLBeEv.js → Register-CiwAT7Hy.js} +5 -6
  118. package/dist/{auth/Register-CPhEO5MG.js.map → demo/Register-CiwAT7Hy.js.map} +1 -1
  119. package/dist/{auth/ResetPassword-DCtGcneA.js → demo/ResetPassword-l9Vg4JE-.js} +4 -5
  120. package/dist/demo/{ResetPassword-D8g9ha1N.js.map → ResetPassword-l9Vg4JE-.js.map} +1 -1
  121. package/dist/demo/{Showcase-D6Fxt4X4.js → Showcase-CX6bDgwe.js} +3 -5
  122. package/dist/demo/{Showcase-D6Fxt4X4.js.map → Showcase-CX6bDgwe.js.map} +1 -1
  123. package/dist/demo/{VerifyEmail-BjDo0cZA.js → VerifyEmail-CAB-OS7i.js} +4 -5
  124. package/dist/demo/{VerifyEmail-BjDo0cZA.js.map → VerifyEmail-CAB-OS7i.js.map} +1 -1
  125. package/dist/demo/{auth-ByVTreDl.js → auth-uegJAdKu.js} +18 -35
  126. package/dist/demo/{auth-ByVTreDl.js.map → auth-uegJAdKu.js.map} +1 -1
  127. package/dist/demo/{core-DFgB3yU4.js → core-B4LVHzPn.js} +132 -93
  128. package/dist/demo/core-B4LVHzPn.js.map +1 -0
  129. package/dist/demo/index.js +20 -23
  130. package/dist/demo/index.js.map +1 -1
  131. package/dist/demo/rolldown-runtime-CiIaOW0V.js +13 -0
  132. package/package.json +17 -20
  133. package/src/admin/AdminRouter.tsx +23 -38
  134. package/src/admin/atoms/adminUserAtom.ts +7 -0
  135. package/src/admin/components/AdminLayout.tsx +2 -14
  136. package/src/admin/components/files/AdminFiles.tsx +123 -1
  137. package/src/admin/components/jobs/{AdminJobExecutions.tsx → AdminJobs.tsx} +450 -317
  138. package/src/admin/components/notifications/AdminNotifications.tsx +11 -25
  139. package/src/admin/components/users/AdminUserLayout.tsx +84 -127
  140. package/src/admin/components/users/AdminUserProfile.tsx +5 -2
  141. package/src/admin/components/users/AdminUsers.tsx +1 -1
  142. package/src/core/components/Flex.tsx +24 -0
  143. package/src/core/components/Section.tsx +109 -0
  144. package/src/core/components/SectionHeader.tsx +106 -0
  145. package/src/core/components/buttons/ActionButton.tsx +1 -0
  146. package/src/core/components/dialogs/PromptDialog.tsx +1 -1
  147. package/src/core/components/layout/Breadcrumb.tsx +2 -2
  148. package/src/core/components/layout/DashboardShell.tsx +1 -1
  149. package/src/core/index.ts +4 -1
  150. package/src/core/services/DialogService.tsx +2 -2
  151. package/src/core/styles.css +2 -1
  152. package/src/core/table/components/DataTable.tsx +5 -2
  153. package/src/demo/DemoRouter.ts +1 -1
  154. package/src/demo/components/auth/DemoLogin.tsx +5 -0
  155. package/src/demo/components/auth/DemoRegister.tsx +5 -0
  156. package/src/demo/components/auth/DemoResetPassword.tsx +5 -0
  157. package/src/demo/components/core/DemoHeading.tsx +56 -3
  158. package/dist/admin/AdminFiles-DFTjijGp.js +0 -111
  159. package/dist/admin/AdminFiles-DFTjijGp.js.map +0 -1
  160. package/dist/admin/AdminJobDashboard-BL8gGPDp.js +0 -354
  161. package/dist/admin/AdminJobDashboard-BL8gGPDp.js.map +0 -1
  162. package/dist/admin/AdminJobExecutions-D9E-CS-U.js.map +0 -1
  163. package/dist/admin/AdminJobRegistry-Ci9ue1zC.js +0 -270
  164. package/dist/admin/AdminJobRegistry-Ci9ue1zC.js.map +0 -1
  165. package/dist/admin/AdminLayout-I6TlUMPc.js.map +0 -1
  166. package/dist/admin/AdminNotifications-ZPHCYrv7.js.map +0 -1
  167. package/dist/admin/AdminUserLayout-lXT6I0Qq.js.map +0 -1
  168. package/dist/admin/AdminUserProfile-vFBLoJ3h.js.map +0 -1
  169. package/dist/admin/AdminUsers-D1UfGya9.js.map +0 -1
  170. package/dist/admin/core-CYaRQ8O-.js.map +0 -1
  171. package/dist/admin/rolldown-runtime-CjeV3_4I.js +0 -18
  172. package/dist/auth/core-D5jIAVF2.js.map +0 -1
  173. package/dist/auth/rolldown-runtime-CjeV3_4I.js +0 -18
  174. package/dist/demo/DemoHeading-C13OVDfS.js +0 -18
  175. package/dist/demo/DemoHeading-C13OVDfS.js.map +0 -1
  176. package/dist/demo/DemoLayout-CHU8WTwO.js.map +0 -1
  177. package/dist/demo/core-DFgB3yU4.js.map +0 -1
  178. package/dist/demo/rolldown-runtime-CjeV3_4I.js +0 -18
  179. package/src/admin/components/jobs/AdminJobDashboard.tsx +0 -349
  180. package/src/admin/components/jobs/AdminJobRegistry.tsx +0 -301
  181. package/src/core/components/Heading.tsx +0 -19
@@ -4,6 +4,7 @@ import {
4
4
  DataTable,
5
5
  DetailList,
6
6
  Flex,
7
+ Section,
7
8
  Text,
8
9
  useToast,
9
10
  } from "@alepha/ui";
@@ -384,19 +385,13 @@ const NotificationDetailContent = ({
384
385
  </Flex>
385
386
 
386
387
  {/* Details */}
387
- <Paper p="sm" radius="md" withBorder>
388
- <Text size="sm" fw={600} mb="xs">
389
- Details
390
- </Text>
388
+ <Section title="Details" p="sm">
391
389
  <DetailList items={detailItems} columns={2} />
392
- </Paper>
390
+ </Section>
393
391
 
394
392
  {/* Rendered Content */}
395
393
  {rendered && (
396
- <Paper p="sm" radius="md" withBorder>
397
- <Text size="sm" fw={600} mb="xs">
398
- Content
399
- </Text>
394
+ <Section title="Content" p="sm">
400
395
  {rendered.type === "email" && (
401
396
  <Flex direction="column" gap="xs">
402
397
  <Flex direction="column" gap={2}>
@@ -455,25 +450,19 @@ const NotificationDetailContent = ({
455
450
  </Flex>
456
451
  </Flex>
457
452
  )}
458
- </Paper>
453
+ </Section>
459
454
  )}
460
455
 
461
456
  {/* Variables */}
462
457
  {detail.variables && Object.keys(detail.variables).length > 0 && (
463
- <Paper p="sm" radius="md" withBorder>
464
- <Text size="sm" fw={600} mb="xs">
465
- Variables
466
- </Text>
458
+ <Section title="Variables" p="sm">
467
459
  <Code block>{JSON.stringify(detail.variables, null, 2)}</Code>
468
- </Paper>
460
+ </Section>
469
461
  )}
470
462
 
471
463
  {/* Error */}
472
464
  {detail.error && (
473
- <Paper p="sm" radius="md" withBorder>
474
- <Text size="sm" fw={600} mb="xs">
475
- Error
476
- </Text>
465
+ <Section title="Error" p="sm">
477
466
  <Paper p="xs" radius="sm" withBorder>
478
467
  <Text
479
468
  size="sm"
@@ -485,15 +474,12 @@ const NotificationDetailContent = ({
485
474
  {detail.error}
486
475
  </Text>
487
476
  </Paper>
488
- </Paper>
477
+ </Section>
489
478
  )}
490
479
 
491
480
  {/* Logs */}
492
481
  {detail.logs && detail.logs.length > 0 && (
493
- <Paper p="sm" radius="md" withBorder>
494
- <Text size="sm" fw={600} mb="xs">
495
- Logs ({detail.logs.length})
496
- </Text>
482
+ <Section title={`Logs (${detail.logs.length})`} p="sm">
497
483
  <Flex
498
484
  direction="column"
499
485
  style={{ maxHeight: 300, overflowY: "auto" }}
@@ -510,7 +496,7 @@ const NotificationDetailContent = ({
510
496
  </Flex>
511
497
  ))}
512
498
  </Flex>
513
- </Paper>
499
+ </Section>
514
500
  )}
515
501
  </Flex>
516
502
  );
@@ -1,47 +1,19 @@
1
- import { Flex, Text, useDialog, useToast } from "@alepha/ui";
2
- import { Loader } from "@mantine/core";
1
+ import { Flex, useDialog, useToast } from "@alepha/ui";
3
2
  import { IconBan, IconShieldCheck, IconTrash } from "@tabler/icons-react";
4
- import { t } from "alepha";
5
3
  import type { AdminUserController, UserEntity } from "alepha/api/users";
6
- import { useClient } from "alepha/react";
7
- import { NestedView, useRouter, useRouterState } from "alepha/react/router";
8
- import {
9
- createContext,
10
- useCallback,
11
- useContext,
12
- useEffect,
13
- useState,
14
- } from "react";
4
+ import { useAlepha, useClient, useStore } from "alepha/react";
5
+ import { NestedView, useRouter } from "alepha/react/router";
6
+ import { useCallback, useEffect } from "react";
15
7
  import type { AdminRouter } from "../../AdminRouter.ts";
8
+ import { adminUserAtom } from "../../atoms/adminUserAtom.ts";
16
9
  import AdminResourceHeader from "../shared/AdminResourceHeader.tsx";
17
10
  import AdminResourceTabs from "../shared/AdminResourceTabs.tsx";
18
11
 
19
12
  export interface AdminUserLayoutProps {
20
- userRealmName?: string;
21
- }
22
-
23
- interface UserContextValue {
24
13
  user: UserEntity;
25
- reload: () => void;
14
+ userRealmName?: string;
26
15
  }
27
16
 
28
- const UserContext = createContext<UserContextValue | null>(null);
29
-
30
- export const useUser = () => {
31
- const ctx = useContext(UserContext);
32
- if (!ctx) throw new Error("useUser must be used within AdminUserLayout");
33
- return ctx;
34
- };
35
-
36
- const updateUserSchema = t.object({
37
- email: t.optional(t.email()),
38
- phoneNumber: t.optional(t.e164()),
39
- firstName: t.optional(t.string()),
40
- lastName: t.optional(t.string()),
41
- roles: t.optional(t.array(t.string())),
42
- enabled: t.optional(t.boolean()),
43
- });
44
-
45
17
  const displayName = (u: UserEntity) =>
46
18
  u.firstName || u.lastName
47
19
  ? `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim()
@@ -49,63 +21,55 @@ const displayName = (u: UserEntity) =>
49
21
 
50
22
  const AdminUserLayout = (props: AdminUserLayoutProps) => {
51
23
  const router = useRouter<AdminRouter>();
52
- const state = useRouterState();
53
24
  const client = useClient<AdminUserController>();
25
+ const alepha = useAlepha();
54
26
  const dialog = useDialog();
55
27
  const toast = useToast();
56
- const userId = state.params.userId as string;
28
+ const [user] = useStore(adminUserAtom);
57
29
 
58
- const [user, setUser] = useState<UserEntity | null>(null);
59
- const [loading, setLoading] = useState(true);
30
+ useEffect(() => {
31
+ alepha.store.set(adminUserAtom, props.user);
32
+ }, [props.user]);
60
33
 
34
+ const currentUser = user ?? props.user;
35
+ const userId = currentUser.id;
61
36
  const realmQuery = { userRealmName: props.userRealmName };
62
37
 
63
- const loadUser = useCallback(async () => {
64
- setLoading(true);
65
- try {
66
- const data = await client.getUser({
67
- params: { id: userId },
68
- query: realmQuery,
69
- });
70
- setUser(data);
71
- } finally {
72
- setLoading(false);
73
- }
38
+ const reload = useCallback(async () => {
39
+ const data = await client.getUser({
40
+ params: { id: userId },
41
+ query: realmQuery,
42
+ });
43
+ alepha.store.set(adminUserAtom, data);
74
44
  }, [userId, client]);
75
45
 
76
- useEffect(() => {
77
- loadUser();
78
- }, [loadUser]);
79
-
80
46
  const handleToggleEnabled = async () => {
81
- if (!user) return;
82
- const action = user.enabled ? "disable" : "enable";
47
+ const action = currentUser.enabled ? "disable" : "enable";
83
48
  const confirmed = await dialog.confirm({
84
- title: `${user.enabled ? "Disable" : "Enable"} User`,
85
- message: `Are you sure you want to ${action} ${displayName(user)}?`,
49
+ title: `${currentUser.enabled ? "Disable" : "Enable"} User`,
50
+ message: `Are you sure you want to ${action} ${displayName(currentUser)}?`,
86
51
  });
87
52
  if (confirmed) {
88
53
  const updated = await client.updateUser({
89
- params: { id: user.id },
54
+ params: { id: currentUser.id },
90
55
  query: realmQuery,
91
- body: { enabled: !user.enabled },
56
+ body: { enabled: !currentUser.enabled },
92
57
  });
93
- setUser(updated);
58
+ alepha.store.set(adminUserAtom, updated);
94
59
  toast.success({ title: `User ${action}d` });
95
60
  }
96
61
  };
97
62
 
98
63
  const handleDelete = async () => {
99
- if (!user) return;
100
64
  const confirmed = await dialog.confirm({
101
65
  title: "Delete User",
102
- message: `Are you sure you want to delete ${displayName(user)}? This cannot be undone.`,
66
+ message: `Are you sure you want to delete ${displayName(currentUser)}? This cannot be undone.`,
103
67
  confirmColor: "red",
104
68
  confirmLabel: "Delete",
105
69
  });
106
70
  if (confirmed) {
107
71
  await client.deleteUser({
108
- params: { id: user.id },
72
+ params: { id: currentUser.id },
109
73
  query: realmQuery,
110
74
  });
111
75
  toast.success({ title: "User deleted" });
@@ -113,71 +77,64 @@ const AdminUserLayout = (props: AdminUserLayoutProps) => {
113
77
  }
114
78
  };
115
79
 
116
- if (loading) {
117
- return (
118
- <Flex flex={1} justify="center" align="center">
119
- <Loader />
120
- </Flex>
121
- );
122
- }
123
-
124
- if (!user) {
125
- return (
126
- <Flex flex={1} justify="center" align="center">
127
- <Text c="dimmed">User not found</Text>
128
- </Flex>
129
- );
130
- }
131
-
132
80
  return (
133
- <UserContext.Provider value={{ user, reload: loadUser }}>
134
- <Flex flex={1} direction="column" gap="lg" p="md">
135
- <AdminResourceHeader
136
- backHref={router.path("adminUsers")}
137
- backLabel="Users"
138
- title={displayName(user)}
139
- subtitle={user.email || user.username}
140
- status={{
141
- label: user.enabled ? "Active" : "Disabled",
142
- color: user.enabled ? "green" : "gray",
143
- }}
144
- menuActions={[
145
- {
146
- label: user.enabled ? "Disable" : "Enable",
147
- icon: user.enabled ? IconBan : IconShieldCheck,
148
- onClick: handleToggleEnabled,
149
- },
150
- {
151
- label: "Delete",
152
- icon: IconTrash,
153
- color: "red",
154
- onClick: handleDelete,
155
- },
156
- ]}
157
- />
158
-
159
- <AdminResourceTabs
160
- tabs={[
161
- {
162
- value: "profile",
163
- label: "Profile",
164
- href: router.path("adminUserProfile", {
165
- params: { userId },
166
- }),
167
- },
168
- {
169
- value: "sessions",
170
- label: "Sessions",
171
- href: router.path("adminUserSessions", {
172
- params: { userId },
173
- }),
174
- },
175
- ]}
176
- />
177
-
178
- <NestedView />
179
- </Flex>
180
- </UserContext.Provider>
81
+ <Flex
82
+ flex={1}
83
+ direction="column"
84
+ gap="lg"
85
+ p="md"
86
+ m="md"
87
+ style={{
88
+ backgroundColor: "var(--mantine-color-body)",
89
+ borderRadius: "var(--mantine-radius-md)",
90
+ border: "1px solid var(--mantine-color-default-border)",
91
+ }}
92
+ >
93
+ <AdminResourceHeader
94
+ backHref={router.path("adminUsers")}
95
+ backLabel="Users"
96
+ title={displayName(currentUser)}
97
+ subtitle={currentUser.email || currentUser.username}
98
+ status={{
99
+ label: currentUser.enabled ? "Active" : "Disabled",
100
+ color: currentUser.enabled ? "green" : "gray",
101
+ }}
102
+ menuActions={[
103
+ {
104
+ label: currentUser.enabled ? "Disable" : "Enable",
105
+ icon: currentUser.enabled ? IconBan : IconShieldCheck,
106
+ onClick: handleToggleEnabled,
107
+ },
108
+ {
109
+ label: "Delete",
110
+ icon: IconTrash,
111
+ color: "red",
112
+ onClick: handleDelete,
113
+ },
114
+ ]}
115
+ />
116
+
117
+ <AdminResourceTabs
118
+ tabs={[
119
+ {
120
+ value: "profile",
121
+ label: "Profile",
122
+ href: router.path("adminUserProfile", {
123
+ params: { userId },
124
+ }),
125
+ },
126
+ {
127
+ value: "sessions",
128
+ label: "Sessions",
129
+ href: router.path("adminUserSessions", {
130
+ params: { userId },
131
+ }),
132
+ },
133
+ ]}
134
+ />
135
+
136
+ <NestedView />
137
+ </Flex>
181
138
  );
182
139
  };
183
140
 
@@ -1,12 +1,15 @@
1
1
  import { DetailList, Flex } from "@alepha/ui";
2
2
  import { Badge } from "@mantine/core";
3
+ import { useStore } from "alepha/react";
3
4
  import { useI18n } from "alepha/react/i18n";
4
- import { useUser } from "./AdminUserLayout.tsx";
5
+ import { adminUserAtom } from "../../atoms/adminUserAtom.ts";
5
6
 
6
7
  const AdminUserProfile = () => {
7
- const { user } = useUser();
8
+ const [user] = useStore(adminUserAtom);
8
9
  const { l } = useI18n();
9
10
 
11
+ if (!user) return null;
12
+
10
13
  return (
11
14
  <DetailList
12
15
  items={[
@@ -152,7 +152,7 @@ const AdminUsers = (props: AdminUsersProps) => {
152
152
  >
153
153
  <Flex gap={"xs"} centerY>
154
154
  <Avatar size={"sm"} name={name} />
155
- <Flex col>
155
+ <Flex col align={"start"}>
156
156
  <Text size={"sm"} bold>
157
157
  {name}
158
158
  </Text>
@@ -2,9 +2,17 @@ import {
2
2
  Flex as MantineFlex,
3
3
  type FlexProps as MantineFlexProps,
4
4
  } from "@mantine/core";
5
+ import { FormModel } from "alepha/react/form";
6
+ import type { FormHTMLAttributes } from "react";
5
7
  import { forwardRef } from "react";
6
8
 
7
9
  export interface FlexProps extends MantineFlexProps {
10
+ /**
11
+ * Render as a `<form>` element.
12
+ * If `true`, renders as a plain form.
13
+ * If an object, spreads the form attributes (onSubmit, id, noValidate, etc.).
14
+ */
15
+ form?: boolean | FormHTMLAttributes<HTMLFormElement> | FormModel<any>;
8
16
  /**
9
17
  * flex: 1 — fill available space.
10
18
  */
@@ -92,6 +100,7 @@ const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
92
100
  borderedBottom,
93
101
  shadowed,
94
102
  overflow,
103
+ form,
95
104
  ...rest
96
105
  } = props;
97
106
 
@@ -155,6 +164,21 @@ const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
155
164
  rest.className = `${rest.className ?? ""} overflow-auto`.trim();
156
165
  }
157
166
 
167
+ if (form) {
168
+ let formProps: any = typeof form === "object" ? form : {};
169
+ if (formProps instanceof FormModel) {
170
+ formProps = formProps.props;
171
+ }
172
+ return (
173
+ <MantineFlex
174
+ ref={ref}
175
+ component={"form"}
176
+ {...(formProps as any)}
177
+ {...rest}
178
+ />
179
+ );
180
+ }
181
+
158
182
  return <MantineFlex ref={ref} {...rest} />;
159
183
  });
160
184
 
@@ -0,0 +1,109 @@
1
+ import { Collapse } from "@mantine/core";
2
+ import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
3
+ import { type ReactNode, useState } from "react";
4
+ import { ui } from "../constants/ui.ts";
5
+ import type { ActionProps } from "./buttons/ActionButton.tsx";
6
+ import Flex from "./Flex.tsx";
7
+ import SectionHeader, { type SectionHeaderProps } from "./SectionHeader.tsx";
8
+
9
+ export interface SectionProps
10
+ extends Omit<SectionHeaderProps, "extra" | "size" | "title"> {
11
+ /**
12
+ * Section header title. When omitted, no header is rendered.
13
+ */
14
+ title?: string | ReactNode;
15
+ /**
16
+ * Section content.
17
+ */
18
+ children?: ReactNode;
19
+
20
+ /**
21
+ * When `true`, the section body can be collapsed/expanded.
22
+ */
23
+ collapsible?: boolean;
24
+
25
+ /**
26
+ * Initial collapsed state. Only applies when `collapsible` is `true`.
27
+ */
28
+ defaultCollapsed?: boolean;
29
+
30
+ /**
31
+ * Padding for the section body.
32
+ */
33
+ p?: string | number;
34
+ }
35
+
36
+ const Section = (props: SectionProps) => {
37
+ const {
38
+ children,
39
+ collapsible,
40
+ defaultCollapsed = false,
41
+ title,
42
+ icon,
43
+ tag,
44
+ subtitle,
45
+ actions,
46
+ p = "md",
47
+ ...rest
48
+ } = props;
49
+
50
+ const [collapsed, setCollapsed] = useState(
51
+ collapsible ? defaultCollapsed : false,
52
+ );
53
+
54
+ const hasHeader = !!title;
55
+
56
+ const headerActions: ActionProps[] = [...(actions ?? [])];
57
+
58
+ const chevronExtra = collapsible ? (
59
+ <Flex
60
+ centerY
61
+ style={{ cursor: "pointer", flexShrink: 0 }}
62
+ onClick={() => setCollapsed((v) => !v)}
63
+ >
64
+ {collapsed ? (
65
+ <IconChevronRight size={ui.sizes.icon.sm} />
66
+ ) : (
67
+ <IconChevronDown size={ui.sizes.icon.sm} />
68
+ )}
69
+ </Flex>
70
+ ) : undefined;
71
+
72
+ return (
73
+ <Flex col surface rounded bordered {...rest}>
74
+ {hasHeader && (
75
+ <Flex
76
+ p="sm"
77
+ px="md"
78
+ style={{
79
+ cursor: collapsible ? "pointer" : undefined,
80
+ }}
81
+ onClick={collapsible ? () => setCollapsed((v) => !v) : undefined}
82
+ >
83
+ <SectionHeader
84
+ icon={icon}
85
+ title={title}
86
+ tag={tag}
87
+ subtitle={subtitle}
88
+ actions={headerActions}
89
+ size="sm"
90
+ extra={chevronExtra}
91
+ />
92
+ </Flex>
93
+ )}
94
+ {collapsible ? (
95
+ <Collapse in={!collapsed}>
96
+ <Flex col p={p} {...(hasHeader ? { borderedTop: true } : {})}>
97
+ {children}
98
+ </Flex>
99
+ </Collapse>
100
+ ) : (
101
+ <Flex col p={p} {...(hasHeader ? { borderedTop: true } : {})}>
102
+ {children}
103
+ </Flex>
104
+ )}
105
+ </Flex>
106
+ );
107
+ };
108
+
109
+ export default Section;
@@ -0,0 +1,106 @@
1
+ import { Title } from "@mantine/core";
2
+ import type { ComponentType, ReactNode } from "react";
3
+ import { ui } from "../constants/ui.ts";
4
+ import { renderIcon } from "../helpers/renderIcon.tsx";
5
+ import type { ActionProps } from "./buttons/ActionButton.tsx";
6
+ import ActionButton from "./buttons/ActionButton.tsx";
7
+ import Flex from "./Flex.tsx";
8
+ import Text from "./Text.tsx";
9
+
10
+ export interface SectionHeaderProps {
11
+ /**
12
+ * Icon displayed before the title.
13
+ * Accepts a React element or a component type (e.g. `IconUser` from tabler).
14
+ */
15
+ icon?: ReactNode | ComponentType<{ size?: number }>;
16
+
17
+ /**
18
+ * Main title text.
19
+ */
20
+ title: string | ReactNode;
21
+
22
+ /**
23
+ * Optional tag displayed after the title (e.g. a Badge or status text).
24
+ */
25
+ tag?: ReactNode;
26
+
27
+ /**
28
+ * Subtitle displayed below the title.
29
+ */
30
+ subtitle?: string | ReactNode;
31
+
32
+ /**
33
+ * Action buttons displayed on the right side.
34
+ */
35
+ actions?: ActionProps[];
36
+
37
+ /**
38
+ * Controls the title size and icon size.
39
+ *
40
+ * - `"sm"` → Title order 5, icon 16px
41
+ * - `"md"` → Title order 4, icon 20px (default)
42
+ * - `"lg"` → Title order 3, icon 24px
43
+ */
44
+ size?: "sm" | "md" | "lg";
45
+
46
+ /**
47
+ * Extra content appended to the right side (after actions).
48
+ */
49
+ extra?: ReactNode;
50
+ }
51
+
52
+ const TITLE_ORDER: Record<string, 1 | 2 | 3 | 4 | 5 | 6> = {
53
+ sm: 5,
54
+ md: 4,
55
+ lg: 3,
56
+ };
57
+
58
+ const ICON_SIZE: Record<string, number> = {
59
+ sm: ui.sizes.icon.xs,
60
+ md: ui.sizes.icon.sm,
61
+ lg: ui.sizes.icon.md,
62
+ };
63
+
64
+ const SectionHeader = (props: SectionHeaderProps) => {
65
+ const { icon, title, tag, subtitle, actions, size = "md", extra } = props;
66
+
67
+ const iconSize = ICON_SIZE[size];
68
+ const titleOrder = TITLE_ORDER[size];
69
+
70
+ return (
71
+ <Flex col gap={2}>
72
+ <Flex centerY gap="xs">
73
+ {icon && (
74
+ <Flex centerY style={{ flexShrink: 0 }}>
75
+ {renderIcon(icon, iconSize)}
76
+ </Flex>
77
+ )}
78
+ <Title order={titleOrder} lh={1.2}>
79
+ {title}
80
+ </Title>
81
+ {tag}
82
+ <Flex fill />
83
+ {actions && actions.length > 0 && (
84
+ <Flex gap="xs" centerY style={{ flexShrink: 0 }}>
85
+ {actions.map((actionProps, index) => (
86
+ <ActionButton
87
+ key={index}
88
+ size="xs"
89
+ variant="default"
90
+ {...actionProps}
91
+ />
92
+ ))}
93
+ </Flex>
94
+ )}
95
+ {extra}
96
+ </Flex>
97
+ {subtitle && (
98
+ <Text muted small>
99
+ {subtitle}
100
+ </Text>
101
+ )}
102
+ </Flex>
103
+ );
104
+ };
105
+
106
+ export default SectionHeader;
@@ -264,6 +264,7 @@ const ActionButton = (_props: ActionProps) => {
264
264
  </ActionHrefButton>
265
265
  );
266
266
  }
267
+
267
268
  return (
268
269
  <ActionNavigationButton {...restProps} href={restProps.href}>
269
270
  {restProps.children}
@@ -36,7 +36,7 @@ const PromptDialog = ({ options, onSubmit }: PromptDialogProps) => {
36
36
  required={options?.required}
37
37
  mb="md"
38
38
  />
39
- <Flex justify="flex-end">
39
+ <Flex justify="flex-end" gap={"xs"}>
40
40
  <Button variant="subtle" onClick={() => onSubmit(null)}>
41
41
  {options?.cancelLabel || "Cancel"}
42
42
  </Button>
@@ -34,7 +34,7 @@ export interface BreadcrumbProps extends FlexProps {
34
34
  * Pages should define a `label` in their `$page()` options for best results.
35
35
  * Falls back to the page name converted to Title Case.
36
36
  */
37
- const Breadcrumb = (props: BreadcrumbProps) => {
37
+ const Breadcrumbs = (props: BreadcrumbProps) => {
38
38
  const { home = "Home", separator, size = "sm", ...groupProps } = props;
39
39
 
40
40
  const state = useRouterState();
@@ -85,4 +85,4 @@ const Breadcrumb = (props: BreadcrumbProps) => {
85
85
  );
86
86
  };
87
87
 
88
- export default Breadcrumb;
88
+ export default Breadcrumbs;