@alepha/ui 0.15.1 → 0.15.2

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 (193) hide show
  1. package/dist/admin/AdminAudits-BlGGKLof.js +3 -0
  2. package/dist/admin/{AdminAudits-DClGEVBj.js → AdminAudits-C0DPYw0W.js} +4 -4
  3. package/dist/admin/AdminAudits-C0DPYw0W.js.map +1 -0
  4. package/dist/admin/AdminFiles-Bg9feLFH.js +3 -0
  5. package/dist/admin/{AdminFiles-C76r1_Xz.js → AdminFiles-Cu8GHgQ3.js} +3 -3
  6. package/dist/admin/AdminFiles-Cu8GHgQ3.js.map +1 -0
  7. package/dist/admin/{AdminNotifications-Bsalygm5.js → AdminNotifications-CgYkBuG_.js} +3 -3
  8. package/dist/admin/AdminNotifications-CgYkBuG_.js.map +1 -0
  9. package/dist/admin/AdminNotifications-DmfGPqHe.js +3 -0
  10. package/dist/admin/{AdminParameters-CpmAWwqN.js → AdminParameters-Cl-R0nXt.js} +1 -1
  11. package/dist/admin/{AdminParameters-Bmxtnpv-.js → AdminParameters-hjNG_KXb.js} +4 -4
  12. package/dist/admin/AdminParameters-hjNG_KXb.js.map +1 -0
  13. package/dist/admin/{AdminSessions-DmK3R6pP.js → AdminSessions-Bey9cuy1.js} +4 -4
  14. package/dist/admin/AdminSessions-Bey9cuy1.js.map +1 -0
  15. package/dist/admin/AdminSessions-Cn4_jB04.js +3 -0
  16. package/dist/admin/{AdminUserAudits-BPMP1Qd2.js → AdminUserAudits-C7AN9jx7.js} +4 -4
  17. package/dist/admin/AdminUserAudits-C7AN9jx7.js.map +1 -0
  18. package/dist/admin/{AdminUserAudits-Brcenss9.js → AdminUserAudits-Cp_ERd2g.js} +1 -1
  19. package/dist/admin/{AdminUserCreate-Cx8bkYC2.js → AdminUserCreate-Chr-7hLk.js} +1 -1
  20. package/dist/admin/{AdminUserCreate-CZjB6NKc.js → AdminUserCreate-DiXi1EWB.js} +4 -4
  21. package/dist/admin/AdminUserCreate-DiXi1EWB.js.map +1 -0
  22. package/dist/admin/{AdminUserDetails-8TYsqQBy.js → AdminUserDetails-Dcn3OwMC.js} +1 -1
  23. package/dist/admin/{AdminUserDetails-DuqCOBJK.js → AdminUserDetails-yM4x8JE6.js} +5 -5
  24. package/dist/admin/AdminUserDetails-yM4x8JE6.js.map +1 -0
  25. package/dist/admin/{AdminUserLayout-Bz2u_zQ4.js → AdminUserLayout-CfeQHH6e.js} +1 -1
  26. package/dist/admin/{AdminUserLayout-Dgk8s7Cd.js → AdminUserLayout-D9bqGt6T.js} +3 -3
  27. package/dist/admin/AdminUserLayout-D9bqGt6T.js.map +1 -0
  28. package/dist/admin/{AdminUserSessions-DCpe8_T6.js → AdminUserSessions-kmkXG-xf.js} +4 -4
  29. package/dist/admin/AdminUserSessions-kmkXG-xf.js.map +1 -0
  30. package/dist/admin/AdminUserSessions-rvA0ztxn.js +3 -0
  31. package/dist/admin/{AdminUserSettings-qxDfowqh.js → AdminUserSettings-BnzRAcqV.js} +4 -4
  32. package/dist/admin/AdminUserSettings-BnzRAcqV.js.map +1 -0
  33. package/dist/admin/AdminUserSettings-CXs-jtRv.js +3 -0
  34. package/dist/admin/{AdminUsers-ZlPsDz0T.js → AdminUsers-CYkcUWCg.js} +4 -4
  35. package/dist/admin/AdminUsers-CYkcUWCg.js.map +1 -0
  36. package/dist/admin/AdminUsers-DdFXzrEn.js +3 -0
  37. package/dist/admin/index.d.ts +33 -18
  38. package/dist/admin/index.d.ts.map +1 -1
  39. package/dist/admin/index.js +44 -29
  40. package/dist/admin/index.js.map +1 -1
  41. package/dist/auth/{AuthLayout-CWzQ8rCe.js → AuthLayout-Dj5K4SIN.js} +2 -2
  42. package/dist/auth/AuthLayout-Dj5K4SIN.js.map +1 -0
  43. package/dist/auth/{Login-CyvKwy5e.js → Login-BAFVcX_J.js} +5 -5
  44. package/dist/auth/Login-BAFVcX_J.js.map +1 -0
  45. package/dist/auth/Login-C5PUsp8I.js +4 -0
  46. package/dist/auth/{Register-C7Zp09Ks.js → Register-CZRXEcWy.js} +6 -6
  47. package/dist/auth/Register-CZRXEcWy.js.map +1 -0
  48. package/dist/auth/Register-DMTs5ep_.js +4 -0
  49. package/dist/auth/ResetPassword-D-mhMtmx.js +3 -0
  50. package/dist/auth/{ResetPassword-DYJSUC6B.js → ResetPassword-DTYNsBIj.js} +5 -5
  51. package/dist/auth/ResetPassword-DTYNsBIj.js.map +1 -0
  52. package/dist/auth/VerifyEmail-BsrCmncc.js +3 -0
  53. package/dist/auth/{VerifyEmail-CNXFIwWW.js → VerifyEmail-DolENWGn.js} +4 -4
  54. package/dist/auth/VerifyEmail-DolENWGn.js.map +1 -0
  55. package/dist/auth/index.d.ts +30 -19
  56. package/dist/auth/index.d.ts.map +1 -1
  57. package/dist/auth/index.js +24 -13
  58. package/dist/auth/index.js.map +1 -1
  59. package/dist/core/index.d.ts +115 -52
  60. package/dist/core/index.d.ts.map +1 -1
  61. package/dist/core/index.js +302 -87
  62. package/dist/core/index.js.map +1 -1
  63. package/dist/demo/{DemoDataTable-DYbDYbs5.js → DemoDataTable-CguplbR7.js} +2 -2
  64. package/dist/demo/{DemoDataTable-DYbDYbs5.js.map → DemoDataTable-CguplbR7.js.map} +1 -1
  65. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +4 -0
  66. package/dist/demo/{DemoJsonViewer-D_Hff1Q2.js → DemoJsonViewer-Dgdk3Txb.js} +3 -3
  67. package/dist/demo/{DemoJsonViewer-D_Hff1Q2.js.map → DemoJsonViewer-Dgdk3Txb.js.map} +1 -1
  68. package/dist/demo/{DemoLayout-DjIDm93B.js → DemoLayout-B20TEuhV.js} +2 -2
  69. package/dist/demo/DemoLayout-B20TEuhV.js.map +1 -0
  70. package/dist/demo/DemoLayout-DSRyf4qJ.js +3 -0
  71. package/dist/demo/{DemoLogin-BA_HiIRZ.js → DemoLogin-mtkN6340.js} +6 -6
  72. package/dist/demo/DemoLogin-mtkN6340.js.map +1 -0
  73. package/dist/demo/{DemoRegister-B6syaxP9.js → DemoRegister-C0MW7anp.js} +7 -7
  74. package/dist/demo/DemoRegister-C0MW7anp.js.map +1 -0
  75. package/dist/demo/{DemoResetPassword-BOcLG4GF.js → DemoResetPassword-CPTy88iK.js} +6 -6
  76. package/dist/demo/DemoResetPassword-CPTy88iK.js.map +1 -0
  77. package/dist/demo/{DemoSidebar-DpZXf7GO.js → DemoSidebar-MVmQKfMt.js} +2 -2
  78. package/dist/demo/{DemoSidebar-DpZXf7GO.js.map → DemoSidebar-MVmQKfMt.js.map} +1 -1
  79. package/dist/demo/{DemoTypeForm-BlLAcQqZ.js → DemoTypeForm-w-qtfRlC.js} +3 -3
  80. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +1 -0
  81. package/dist/demo/{DemoVerifyEmail-C-J7bXUQ.js → DemoVerifyEmail-C8FFJT5A.js} +5 -5
  82. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +1 -0
  83. package/dist/demo/{Showcase-HchhcsHV.js → Showcase-CQrMWars.js} +2 -2
  84. package/dist/demo/Showcase-CQrMWars.js.map +1 -0
  85. package/dist/demo/index.d.ts +25 -15
  86. package/dist/demo/index.d.ts.map +1 -1
  87. package/dist/demo/index.js +24 -14
  88. package/dist/demo/index.js.map +1 -1
  89. package/package.json +5 -6
  90. package/src/admin/AdminRouter.ts +2 -2
  91. package/src/admin/MainRouter.ts +1 -1
  92. package/src/admin/components/audits/AdminAudits.tsx +3 -3
  93. package/src/admin/components/files/AdminFiles.tsx +2 -2
  94. package/src/admin/components/jobs/AdminJobs.tsx +2 -2
  95. package/src/admin/components/notifications/AdminNotifications.tsx +2 -2
  96. package/src/admin/components/parameters/AdminParameters.tsx +1 -1
  97. package/src/admin/components/parameters/ParameterDetails.tsx +2 -2
  98. package/src/admin/components/parameters/ParameterHistory.tsx +1 -1
  99. package/src/admin/components/parameters/types.ts +9 -3
  100. package/src/admin/components/sessions/AdminSessions.tsx +3 -3
  101. package/src/admin/components/shared/AdminResourceHeader.tsx +1 -1
  102. package/src/admin/components/shared/AdminResourceTabs.tsx +1 -1
  103. package/src/admin/components/users/AdminUserAudits.tsx +3 -3
  104. package/src/admin/components/users/AdminUserCreate.tsx +3 -3
  105. package/src/admin/components/users/AdminUserDetails.tsx +4 -4
  106. package/src/admin/components/users/AdminUserLayout.tsx +2 -2
  107. package/src/admin/components/users/AdminUserSessions.tsx +3 -3
  108. package/src/admin/components/users/AdminUserSettings.tsx +3 -3
  109. package/src/admin/components/users/AdminUsers.tsx +3 -3
  110. package/src/admin/index.ts +16 -1
  111. package/src/auth/AuthI18n.ts +1 -1
  112. package/src/auth/AuthRouter.ts +2 -2
  113. package/src/auth/components/AuthLayout.tsx +1 -1
  114. package/src/auth/components/Login.tsx +4 -4
  115. package/src/auth/components/Register.tsx +5 -5
  116. package/src/auth/components/ResetPassword.tsx +4 -4
  117. package/src/auth/components/VerifyEmail.tsx +3 -3
  118. package/src/auth/components/buttons/UserButton.tsx +2 -2
  119. package/src/auth/index.ts +14 -3
  120. package/src/core/RootRouter.ts +1 -1
  121. package/src/core/atoms/alephaSidebarAtom.ts +57 -0
  122. package/src/core/components/buttons/ActionButton.tsx +9 -9
  123. package/src/core/components/buttons/BurgerButton.tsx +5 -4
  124. package/src/core/components/buttons/LanguageButton.tsx +1 -1
  125. package/src/core/components/buttons/OmnibarButton.tsx +20 -1
  126. package/src/core/components/buttons/ThemeButton.tsx +1 -1
  127. package/src/core/components/buttons/ToggleSidebarButton.tsx +33 -23
  128. package/src/core/components/form/Control.tsx +1 -1
  129. package/src/core/components/form/ControlArray.tsx +2 -2
  130. package/src/core/components/form/ControlDate.tsx +1 -1
  131. package/src/core/components/form/ControlNumber.tsx +2 -2
  132. package/src/core/components/form/ControlObject.tsx +1 -1
  133. package/src/core/components/form/ControlQueryBuilder.tsx +1 -1
  134. package/src/core/components/form/ControlSelect.tsx +1 -1
  135. package/src/core/components/form/TypeForm.tsx +2 -2
  136. package/src/core/components/layout/AdminShell.tsx +205 -27
  137. package/src/core/components/layout/AlephaMantineProvider.tsx +3 -3
  138. package/src/core/components/layout/Omnibar.tsx +2 -2
  139. package/src/core/components/layout/Sidebar.tsx +42 -77
  140. package/src/core/components/table/DataTable.tsx +2 -2
  141. package/src/core/components/table/DataTableFilters.tsx +1 -1
  142. package/src/core/components/table/types.ts +1 -1
  143. package/src/core/hooks/useDialog.ts +1 -1
  144. package/src/core/hooks/useTheme.ts +1 -1
  145. package/src/core/hooks/useToast.ts +1 -1
  146. package/src/core/index.ts +57 -6
  147. package/src/core/providers/ThemeProvider.ts +1 -1
  148. package/src/core/styles.css +58 -0
  149. package/src/core/utils/parseInput.ts +1 -1
  150. package/src/demo/DemoRouter.ts +1 -1
  151. package/src/demo/components/DemoLayout.tsx +1 -1
  152. package/src/demo/components/core/DemoTypeForm.tsx +1 -1
  153. package/src/demo/components/json/DemoJsonViewer.tsx +1 -1
  154. package/src/demo/components/shared/Showcase.tsx +1 -1
  155. package/src/demo/index.ts +11 -1
  156. package/src/json/index.ts +13 -0
  157. package/dist/admin/AdminAudits-ColpiP4T.js +0 -3
  158. package/dist/admin/AdminAudits-DClGEVBj.js.map +0 -1
  159. package/dist/admin/AdminFiles-C5pqXN5B.js +0 -3
  160. package/dist/admin/AdminFiles-C76r1_Xz.js.map +0 -1
  161. package/dist/admin/AdminNotifications-BXixCBu9.js +0 -3
  162. package/dist/admin/AdminNotifications-Bsalygm5.js.map +0 -1
  163. package/dist/admin/AdminParameters-Bmxtnpv-.js.map +0 -1
  164. package/dist/admin/AdminSessions-CrkRvey3.js +0 -3
  165. package/dist/admin/AdminSessions-DmK3R6pP.js.map +0 -1
  166. package/dist/admin/AdminUserAudits-BPMP1Qd2.js.map +0 -1
  167. package/dist/admin/AdminUserCreate-CZjB6NKc.js.map +0 -1
  168. package/dist/admin/AdminUserDetails-DuqCOBJK.js.map +0 -1
  169. package/dist/admin/AdminUserLayout-Dgk8s7Cd.js.map +0 -1
  170. package/dist/admin/AdminUserSessions-DCpe8_T6.js.map +0 -1
  171. package/dist/admin/AdminUserSessions-beiJqY2D.js +0 -3
  172. package/dist/admin/AdminUserSettings-CxlInVnu.js +0 -3
  173. package/dist/admin/AdminUserSettings-qxDfowqh.js.map +0 -1
  174. package/dist/admin/AdminUsers-Bd0wMP8v.js +0 -3
  175. package/dist/admin/AdminUsers-ZlPsDz0T.js.map +0 -1
  176. package/dist/auth/AuthLayout-CWzQ8rCe.js.map +0 -1
  177. package/dist/auth/Login-CxOPyNFP.js +0 -4
  178. package/dist/auth/Login-CyvKwy5e.js.map +0 -1
  179. package/dist/auth/Register-C7Zp09Ks.js.map +0 -1
  180. package/dist/auth/Register-Cacr7YbA.js +0 -4
  181. package/dist/auth/ResetPassword-CMkx8Ibf.js +0 -3
  182. package/dist/auth/ResetPassword-DYJSUC6B.js.map +0 -1
  183. package/dist/auth/VerifyEmail-CNXFIwWW.js.map +0 -1
  184. package/dist/auth/VerifyEmail-DKyDlz96.js +0 -3
  185. package/dist/demo/DemoJsonViewer-DbWVDdz_.js +0 -4
  186. package/dist/demo/DemoLayout-DjIDm93B.js.map +0 -1
  187. package/dist/demo/DemoLayout-nNMajP_9.js +0 -3
  188. package/dist/demo/DemoLogin-BA_HiIRZ.js.map +0 -1
  189. package/dist/demo/DemoRegister-B6syaxP9.js.map +0 -1
  190. package/dist/demo/DemoResetPassword-BOcLG4GF.js.map +0 -1
  191. package/dist/demo/DemoTypeForm-BlLAcQqZ.js.map +0 -1
  192. package/dist/demo/DemoVerifyEmail-C-J7bXUQ.js.map +0 -1
  193. package/dist/demo/Showcase-HchhcsHV.js.map +0 -1
@@ -1,5 +1,3 @@
1
- import { useEvents, useStore } from "@alepha/react";
2
- import { NestedView, useRouter } from "@alepha/react/router";
3
1
  import {
4
2
  AppShell,
5
3
  type AppShellFooterProps,
@@ -7,8 +5,18 @@ import {
7
5
  type AppShellMainProps,
8
6
  type AppShellNavbarProps,
9
7
  type AppShellProps,
8
+ Flex,
10
9
  } from "@mantine/core";
11
- import { type ReactNode, useState } from "react";
10
+ import { useEvents, useStore } from "alepha/react";
11
+ import { NestedView, useRouter } from "alepha/react/router";
12
+ import {
13
+ type ReactNode,
14
+ useCallback,
15
+ useEffect,
16
+ useRef,
17
+ useState,
18
+ } from "react";
19
+ import { alephaSidebarAtom } from "../../atoms/alephaSidebarAtom.ts";
12
20
  import { ui } from "../../constants/ui.ts";
13
21
  import AppBar, { type AppBarProps } from "./AppBar.tsx";
14
22
  import { Sidebar, type SidebarProps } from "./Sidebar.tsx";
@@ -25,6 +33,12 @@ export interface AdminShellProps {
25
33
  footer?: ReactNode;
26
34
  children?: ReactNode;
27
35
 
36
+ /**
37
+ * Enable drag-to-resize for the sidebar.
38
+ * Width and constraints are configured in alephaSidebarAtom.
39
+ */
40
+ resizable?: boolean;
41
+
28
42
  noSidebarWhen?: {
29
43
  /**
30
44
  * Paths where the sidebar should be hidden.
@@ -33,28 +47,143 @@ export interface AdminShellProps {
33
47
  };
34
48
  }
35
49
 
36
- declare module "alepha" {
37
- interface State {
38
- /**
39
- * Whether the sidebar is opened or closed.
40
- */
41
- "alepha.ui.sidebar.opened"?: boolean;
42
-
43
- /**
44
- * Whether the sidebar is collapsed (narrow) or expanded (wide).
45
- */
46
- "alepha.ui.sidebar.collapsed"?: boolean;
47
- }
48
- }
49
-
50
50
  const AdminShell = (props: AdminShellProps) => {
51
51
  const router = useRouter();
52
- const [opened, setOpened] = useStore("alepha.ui.sidebar.opened");
53
- const [collapsed] = useStore(
54
- "alepha.ui.sidebar.collapsed",
55
- props.sidebarProps?.collapsed,
52
+ const [sidebar, setSidebar] = useStore(alephaSidebarAtom);
53
+ const { opened, collapsed } = sidebar;
54
+
55
+ // Initialize collapsed state from props on mount
56
+ useEffect(() => {
57
+ if (props.sidebarProps?.collapsed !== undefined) {
58
+ setSidebar({ ...sidebar, collapsed: props.sidebarProps.collapsed });
59
+ }
60
+ }, []);
61
+
62
+ // Resize state
63
+ const [isResizing, setIsResizing] = useState(false);
64
+ const [isHovering, setIsHovering] = useState(false);
65
+ const [collapseEffect, setCollapseEffect] = useState({
66
+ offset: 0,
67
+ opacity: 1,
68
+ });
69
+ const resizeRef = useRef<{ startX: number; startWidth: number } | null>(null);
70
+
71
+ // Use atom values for constraints
72
+ const {
73
+ collapsedWidth,
74
+ collapseThreshold,
75
+ maxWidth,
76
+ hoverDelay,
77
+ defaultWidth,
78
+ } = sidebar;
79
+
80
+ const handleResizeStart = useCallback(
81
+ (e: React.MouseEvent) => {
82
+ if (!props.resizable) return;
83
+ e.preventDefault();
84
+
85
+ // If collapsed and hovering, un-collapse first and start from defaultWidth
86
+ if (collapsed) {
87
+ setSidebar({ ...sidebar, collapsed: false, width: defaultWidth });
88
+ setIsResizing(true);
89
+ resizeRef.current = {
90
+ startX: e.clientX,
91
+ startWidth: defaultWidth,
92
+ };
93
+ } else {
94
+ setIsResizing(true);
95
+ resizeRef.current = {
96
+ startX: e.clientX,
97
+ startWidth: sidebar.width,
98
+ };
99
+ }
100
+ },
101
+ [props.resizable, collapsed, sidebar, setSidebar, defaultWidth],
56
102
  );
57
103
 
104
+ useEffect(() => {
105
+ if (!isResizing) return;
106
+
107
+ const handleMouseMove = (e: MouseEvent) => {
108
+ if (!resizeRef.current) return;
109
+ const delta = e.clientX - resizeRef.current.startX;
110
+ const rawWidth = resizeRef.current.startWidth + delta;
111
+ const newWidth = Math.min(Math.max(rawWidth, collapsedWidth), maxWidth);
112
+
113
+ // Visual effect when below collapse threshold
114
+ if (rawWidth < collapseThreshold) {
115
+ const progress = Math.max(
116
+ 0,
117
+ (collapseThreshold - rawWidth) / collapseThreshold,
118
+ );
119
+ setCollapseEffect({
120
+ offset: -progress * collapsedWidth,
121
+ opacity: 1 - progress * 0.7,
122
+ });
123
+ setSidebar({ ...sidebar, width: collapseThreshold, collapsed: false });
124
+ } else {
125
+ setCollapseEffect({ offset: 0, opacity: 1 });
126
+ setSidebar({ ...sidebar, width: newWidth, collapsed: false });
127
+ }
128
+ };
129
+
130
+ const handleMouseUp = () => {
131
+ // If we released while in collapse zone, actually collapse
132
+ if (collapseEffect.offset < 0) {
133
+ setSidebar({ ...sidebar, collapsed: true });
134
+ }
135
+ setCollapseEffect({ offset: 0, opacity: 1 });
136
+ setIsResizing(false);
137
+ resizeRef.current = null;
138
+ };
139
+
140
+ document.addEventListener("mousemove", handleMouseMove);
141
+ document.addEventListener("mouseup", handleMouseUp);
142
+
143
+ return () => {
144
+ document.removeEventListener("mousemove", handleMouseMove);
145
+ document.removeEventListener("mouseup", handleMouseUp);
146
+ };
147
+ }, [
148
+ isResizing,
149
+ sidebar,
150
+ setSidebar,
151
+ collapsedWidth,
152
+ maxWidth,
153
+ collapseThreshold,
154
+ collapseEffect.offset,
155
+ ]);
156
+
157
+ // Hover to expand when collapsed (with delay)
158
+ const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
159
+
160
+ const handleNavbarMouseEnter = useCallback(() => {
161
+ if (collapsed) {
162
+ hoverTimeoutRef.current = setTimeout(() => {
163
+ setIsHovering(true);
164
+ }, hoverDelay);
165
+ }
166
+ }, [collapsed, hoverDelay]);
167
+
168
+ const handleNavbarMouseLeave = useCallback(() => {
169
+ if (hoverTimeoutRef.current) {
170
+ clearTimeout(hoverTimeoutRef.current);
171
+ hoverTimeoutRef.current = null;
172
+ }
173
+ setIsHovering(false);
174
+ }, []);
175
+
176
+ // Reset hover state when collapsed changes (e.g., when toggle button is clicked)
177
+ useEffect(() => {
178
+ if (collapsed) {
179
+ setIsHovering(false);
180
+ if (hoverTimeoutRef.current) {
181
+ clearTimeout(hoverTimeoutRef.current);
182
+ hoverTimeoutRef.current = null;
183
+ }
184
+ }
185
+ }, [collapsed]);
186
+
58
187
  const shouldShowSidebar = () => {
59
188
  if (props.noSidebarWhen?.paths) {
60
189
  for (const path of props.noSidebarWhen.paths) {
@@ -78,10 +207,10 @@ const AdminShell = (props: AdminShellProps) => {
78
207
  setShowSidebar(shouldShowSidebar());
79
208
  },
80
209
  "react:transition:begin": () => {
81
- setOpened(false);
210
+ setSidebar({ ...sidebar, opened: false });
82
211
  },
83
212
  },
84
- [],
213
+ [sidebar],
85
214
  );
86
215
 
87
216
  // Default AppBar items with burger button on the left
@@ -94,7 +223,20 @@ const AdminShell = (props: AdminShellProps) => {
94
223
 
95
224
  const headerHeight = hasAppBar ? 60 : 0;
96
225
  const footerHeight = props.footer ? 24 : 0;
97
- const sidebarWidth = hasSidebar ? (collapsed ? 78 : 300) : 0;
226
+ const expandedWidth = Math.max(sidebar.width, collapsedWidth);
227
+
228
+ // When collapsed but hovering, show defaultWidth (not current width)
229
+ const isExpandedByHover = collapsed && isHovering;
230
+ const effectiveCollapsed = collapsed && !isHovering;
231
+ const hoverWidth = Math.max(defaultWidth, collapsedWidth);
232
+ const sidebarWidth = hasSidebar
233
+ ? effectiveCollapsed
234
+ ? collapsedWidth
235
+ : isExpandedByHover
236
+ ? hoverWidth
237
+ : expandedWidth
238
+ : 0;
239
+ const canResize = props.resizable && !collapsed;
98
240
 
99
241
  return (
100
242
  <AppShell
@@ -105,7 +247,11 @@ const AdminShell = (props: AdminShellProps) => {
105
247
  navbar={
106
248
  hasSidebar
107
249
  ? {
108
- width: collapsed ? { base: 78 } : { base: 300 },
250
+ width: effectiveCollapsed
251
+ ? { base: collapsedWidth }
252
+ : isExpandedByHover
253
+ ? { base: hoverWidth }
254
+ : { base: expandedWidth },
109
255
  breakpoint: "sm",
110
256
  collapsed: { mobile: !opened },
111
257
  }
@@ -121,8 +267,38 @@ const AdminShell = (props: AdminShellProps) => {
121
267
  </AppShell.Header>
122
268
 
123
269
  {hasSidebar && (
124
- <AppShell.Navbar bg={ui.colors.surface} {...props.appShellNavbarProps}>
125
- <Sidebar collapsed={collapsed} {...(props.sidebarProps ?? {})} />
270
+ <AppShell.Navbar
271
+ bg={ui.colors.surface}
272
+ className="alepha-sidebar-navbar"
273
+ data-resizing={isResizing}
274
+ onMouseEnter={handleNavbarMouseEnter}
275
+ onMouseLeave={handleNavbarMouseLeave}
276
+ style={{
277
+ transform: collapseEffect.offset
278
+ ? `translateX(${collapseEffect.offset}px)`
279
+ : undefined,
280
+ opacity: collapseEffect.opacity,
281
+ }}
282
+ {...props.appShellNavbarProps}
283
+ >
284
+ <Sidebar
285
+ {...(props.sidebarProps ?? {})}
286
+ collapsed={effectiveCollapsed}
287
+ />
288
+ {(canResize || isExpandedByHover) && (
289
+ <Flex
290
+ pos="absolute"
291
+ right={-2}
292
+ top={0}
293
+ bottom={0}
294
+ w={4}
295
+ style={{
296
+ cursor: "col-resize",
297
+ userSelect: "none",
298
+ }}
299
+ onMouseDown={handleResizeStart}
300
+ />
301
+ )}
126
302
  </AppShell.Navbar>
127
303
  )}
128
304
 
@@ -134,6 +310,8 @@ const AdminShell = (props: AdminShellProps) => {
134
310
  display={"flex"}
135
311
  flex={1}
136
312
  style={{ flexDirection: "column" }}
313
+ className="alepha-sidebar-main"
314
+ data-resizing={isResizing}
137
315
  {...props.appShellMainProps}
138
316
  >
139
317
  {props.children ?? <NestedView />}
@@ -1,6 +1,3 @@
1
- import { useEvents } from "@alepha/react";
2
- import { FormValidationError } from "@alepha/react/form";
3
- import { NestedView } from "@alepha/react/router";
4
1
  import {
5
2
  ColorSchemeScript,
6
3
  type ColorSchemeScriptProps,
@@ -12,6 +9,9 @@ import { Notifications, type NotificationsProps } from "@mantine/notifications";
12
9
  import type { NavigationProgressProps } from "@mantine/nprogress";
13
10
  import { NavigationProgress, nprogress } from "@mantine/nprogress";
14
11
  import { TypeBoxError } from "alepha";
12
+ import { useEvents } from "alepha/react";
13
+ import { FormValidationError } from "alepha/react/form";
14
+ import { NestedView } from "alepha/react/router";
15
15
  import type { ReactNode } from "react";
16
16
  import { useTheme } from "../../hooks/useTheme.ts";
17
17
  import { useToast } from "../../hooks/useToast.ts";
@@ -1,7 +1,7 @@
1
- import { useStore } from "@alepha/react";
2
- import { useRouter } from "@alepha/react/router";
3
1
  import { Spotlight, type SpotlightActionData } from "@mantine/spotlight";
4
2
  import { IconSearch } from "@tabler/icons-react";
3
+ import { useStore } from "alepha/react";
4
+ import { useRouter } from "alepha/react/router";
5
5
  import { type ReactNode, useMemo } from "react";
6
6
  import { ui } from "../../constants/ui.ts";
7
7
  import { renderIcon } from "../../helpers/renderIcon.tsx";
@@ -1,5 +1,3 @@
1
- import { useEvents } from "@alepha/react";
2
- import { useRouter } from "@alepha/react/router";
3
1
  import {
4
2
  Flex,
5
3
  type FlexProps,
@@ -11,6 +9,8 @@ import {
11
9
  IconChevronRight,
12
10
  IconSquareRounded,
13
11
  } from "@tabler/icons-react";
12
+ import { useEvents } from "alepha/react";
13
+ import { useRouter } from "alepha/react/router";
14
14
  import {
15
15
  type ComponentType,
16
16
  type ReactNode,
@@ -49,44 +49,51 @@ export const Sidebar = (props: SidebarProps) => {
49
49
  const router = useRouter();
50
50
  const { onItemClick } = props;
51
51
 
52
+ const divider = (key: string | number) => {
53
+ return (
54
+ <Flex
55
+ key={key}
56
+ h={1}
57
+ bg={"var(--alepha-border)"}
58
+ my={"xs"}
59
+ mx={props.collapsed ? 0 : "sm"}
60
+ />
61
+ );
62
+ };
63
+
52
64
  const renderNode = (item: SidebarNode, key: number) => {
53
65
  if ("type" in item) {
66
+ // Hide spacers when collapsed
54
67
  if (item.type === "spacer") {
68
+ if (props.collapsed) return null;
55
69
  return <Flex key={key} h={16} />;
56
70
  }
57
71
 
58
72
  if (item.type === "divider") {
59
- return (
60
- <Flex
61
- key={key}
62
- h={1}
63
- bg={"var(--alepha-border)"}
64
- my={"md"}
65
- mx={"sm"}
66
- />
67
- );
73
+ return divider(key);
68
74
  }
69
75
 
70
76
  if (item.type === "search") {
71
- return <OmnibarButton collapsed={props.collapsed} key={key} />;
77
+ return (
78
+ <Flex key={key} mb="xs">
79
+ <OmnibarButton collapsed={props.collapsed} />
80
+ </Flex>
81
+ );
72
82
  }
73
83
 
74
84
  if (item.type === "toggle") {
75
85
  return <ToggleSidebarButton key={key} />;
76
86
  }
77
87
 
88
+ // Replace sections with dividers when collapsed
78
89
  if (item.type === "section") {
79
- if (props.collapsed) return;
90
+ if (props.collapsed) {
91
+ return divider(key);
92
+ }
80
93
  return (
81
- <Flex key={key} mt={"md"} mb={"xs"} align={"center"} gap={"xs"}>
94
+ <Flex key={key} mt={"md"} align={"center"} gap={"xs"}>
82
95
  {renderIcon(item.icon)}
83
- <Text
84
- key={key}
85
- size={"xs"}
86
- c={"dimmed"}
87
- tt={"uppercase"}
88
- fw={"bold"}
89
- >
96
+ <Text size={"xs"} c={"dimmed"} tt={"uppercase"} fw={"bold"}>
90
97
  {item.label}
91
98
  </Text>
92
99
  </Flex>
@@ -150,15 +157,18 @@ export const Sidebar = (props: SidebarProps) => {
150
157
  };
151
158
 
152
159
  const padding = "md";
153
- const gap = props.items ? props.gap : "xs";
154
- const menu = useMemo(() => getSidebarNodes(), []);
160
+ const gap = props.items ? (props.gap ?? 2) : "xs";
161
+ const menu = useMemo(
162
+ () => getSidebarNodes(),
163
+ [props.items, props.autoPopulateMenu],
164
+ );
155
165
 
156
166
  return (
157
167
  <Flex
158
168
  flex={1}
159
169
  py={padding}
160
170
  direction={"column"}
161
- className={"overflow-auto"}
171
+ className="alepha-sidebar-scroll"
162
172
  {...props.flexProps}
163
173
  >
164
174
  <Flex gap={gap} px={padding} direction={"column"}>
@@ -171,7 +181,7 @@ export const Sidebar = (props: SidebarProps) => {
171
181
  px={padding}
172
182
  direction={"column"}
173
183
  flex={1}
174
- className={"overflow-auto"}
184
+ className="alepha-sidebar-scroll"
175
185
  >
176
186
  {menu
177
187
  .filter((it) => !it.position)
@@ -332,34 +342,9 @@ export interface SidebarItemProps {
332
342
  const SidebarCollapsedItem = (props: SidebarItemProps) => {
333
343
  const { item, level } = props;
334
344
 
335
- const router = useRouter();
336
- const isActive = useCallback((item: SidebarMenuItem): boolean => {
337
- if (!item.children) return false;
338
- for (const child of item.children) {
339
- if (child.href) {
340
- if (router.isActive(child.href)) {
341
- return true;
342
- }
343
- }
344
- if (isActive(child)) {
345
- return true;
346
- }
347
- }
348
- return false;
349
- }, []);
350
-
351
- const [isOpen, setIsOpen] = useState<boolean>(isActive(item));
352
-
353
- const handleItemClick = (e: MouseEvent) => {
354
- if (!props.item.target) {
355
- e.preventDefault();
356
- }
357
- if (item.children && item.children.length > 0) {
358
- setIsOpen(!isOpen);
359
- } else {
360
- props.onItemClick?.(item);
361
- item.onClick?.();
362
- }
345
+ const handleItemClick = () => {
346
+ props.onItemClick?.(item);
347
+ item.onClick?.();
363
348
  };
364
349
 
365
350
  return (
@@ -371,35 +356,15 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
371
356
  }
372
357
  variant={"subtle"}
373
358
  variantActive={"default"}
374
- tooltip={
375
- item.children
376
- ? undefined
377
- : {
378
- label: item.label,
379
- position: "right",
380
- }
381
- }
359
+ tooltip={{
360
+ label: item.label,
361
+ position: "right",
362
+ }}
382
363
  radius={props.item.theme?.radius ?? props.theme.button?.radius ?? "md"}
383
364
  onClick={handleItemClick}
384
365
  icon={renderIcon(item.icon) ?? <IconSquareRounded />}
385
366
  href={props.item.href as any}
386
367
  target={props.item.target}
387
- menu={
388
- item.children
389
- ? ({
390
- position: "right",
391
- on: "hover",
392
- items: item.children
393
- .filter((child) => !child.can || child.can())
394
- .map((child) => ({
395
- label: child.label,
396
- href: child.href,
397
- icon: renderIcon(child.icon),
398
- children: child.children?.filter((c) => !c.can || c.can()),
399
- })),
400
- } as any)
401
- : undefined
402
- }
403
368
  {...props.item.actionProps}
404
369
  />
405
370
  );
@@ -1,5 +1,3 @@
1
- import { useInject } from "@alepha/react";
2
- import { type FormModel, useForm } from "@alepha/react/form";
3
1
  import { Checkbox, Flex, Table, Text, UnstyledButton } from "@mantine/core";
4
2
  import { useDebouncedCallback } from "@mantine/hooks";
5
3
  import {
@@ -9,6 +7,8 @@ import {
9
7
  } from "@tabler/icons-react";
10
8
  import { Alepha, type Static, type TObject, t } from "alepha";
11
9
  import { DateTimeProvider } from "alepha/datetime";
10
+ import { useInject } from "alepha/react";
11
+ import { type FormModel, useForm } from "alepha/react/form";
12
12
  import { useCallback, useEffect, useMemo, useState } from "react";
13
13
  import { ui } from "../../constants/ui.ts";
14
14
  import DataTableFilters, {
@@ -1,6 +1,6 @@
1
- import type { FormModel } from "@alepha/react/form";
2
1
  import { Flex } from "@mantine/core";
3
2
  import { type TObject, t } from "alepha";
3
+ import type { FormModel } from "alepha/react/form";
4
4
  import { useMemo } from "react";
5
5
  import { ui } from "../../constants/ui.ts";
6
6
  import TypeForm, { type TypeFormProps } from "../form/TypeForm.tsx";
@@ -1,4 +1,3 @@
1
- import type { FormModel } from "@alepha/react/form";
2
1
  import type { TableProps, TableTrProps } from "@mantine/core";
3
2
  import type {
4
3
  Alepha,
@@ -9,6 +8,7 @@ import type {
9
8
  TObject,
10
9
  } from "alepha";
11
10
  import type { DurationLike } from "alepha/datetime";
11
+ import type { FormModel } from "alepha/react/form";
12
12
  import type { ReactNode } from "react";
13
13
  import type { ActionProps } from "../buttons/ActionButton.tsx";
14
14
  import type { TypeFormProps } from "../form/TypeForm.tsx";
@@ -1,4 +1,4 @@
1
- import { useInject } from "@alepha/react";
1
+ import { useInject } from "alepha/react";
2
2
  import { DialogService } from "../services/DialogService.tsx";
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { useInject, useStore } from "@alepha/react";
1
+ import { useInject, useStore } from "alepha/react";
2
2
  import {
3
3
  alephaThemeAtom,
4
4
  type CurrentAlephaTheme,
@@ -1,4 +1,4 @@
1
- import { useInject } from "@alepha/react";
1
+ import { useInject } from "alepha/react";
2
2
  import { ToastService } from "../services/ToastService.tsx";
3
3
 
4
4
  /**
package/src/core/index.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { AlephaReactForm } from "@alepha/react/form";
2
- import { AlephaReactHead } from "@alepha/react/head";
3
- import { AlephaReactI18n } from "@alepha/react/i18n";
4
- import { $module, type Static } from "alepha";
1
+ import { $context, $module, type Static } from "alepha";
2
+ import { AlephaReactForm } from "alepha/react/form";
3
+ import { AlephaReactHead } from "alepha/react/head";
4
+ import { AlephaReactI18n } from "alepha/react/i18n";
5
5
  import type { ComponentType, ReactNode } from "react";
6
+ import { alephaSidebarAtom } from "./atoms/alephaSidebarAtom.ts";
6
7
  import { alephaThemeAtom } from "./atoms/alephaThemeAtom.ts";
7
8
  import type { ControlProps } from "./components/form/Control.tsx";
8
9
  import { ThemeProvider } from "./providers/ThemeProvider.ts";
@@ -13,6 +14,7 @@ import { ToastService } from "./services/ToastService.tsx";
13
14
  // ---------------------------------------------------------------------------------------------------------------------
14
15
 
15
16
  export { Flex, Text } from "@mantine/core";
17
+ export * from "./atoms/alephaSidebarAtom.ts";
16
18
  export * from "./atoms/alephaThemeAtom.ts";
17
19
  export * from "./atoms/alephaThemeListAtom.ts";
18
20
  export * from "./atoms/themes/default.ts";
@@ -35,6 +37,7 @@ export { default as LanguageButton } from "./components/buttons/LanguageButton.t
35
37
  export { default as OmnibarButton } from "./components/buttons/OmnibarButton.tsx";
36
38
  export type { ThemeButtonProps } from "./components/buttons/ThemeButton.tsx";
37
39
  export { default as ThemeButton } from "./components/buttons/ThemeButton.tsx";
40
+ export { default as ToggleSidebarButton } from "./components/buttons/ToggleSidebarButton.tsx";
38
41
  export { default as AlertDialog } from "./components/dialogs/AlertDialog.tsx";
39
42
  export { default as ConfirmDialog } from "./components/dialogs/ConfirmDialog.tsx";
40
43
  export { default as PromptDialog } from "./components/dialogs/PromptDialog.tsx";
@@ -121,11 +124,12 @@ declare module "typebox" {
121
124
 
122
125
  declare module "alepha" {
123
126
  interface State {
127
+ [alephaSidebarAtom.key]?: Static<typeof alephaSidebarAtom.schema>;
124
128
  [alephaThemeAtom.key]?: Static<typeof alephaThemeAtom.schema>;
125
129
  }
126
130
  }
127
131
 
128
- declare module "@alepha/react/router" {
132
+ declare module "alepha/react/router" {
129
133
  interface PagePrimitiveOptions {
130
134
  /**
131
135
  * Human-readable title for the page.
@@ -152,7 +156,25 @@ declare module "@alepha/react/router" {
152
156
  // ---------------------------------------------------------------------------------------------------------------------
153
157
 
154
158
  /**
155
- * Mantine
159
+ * | type | quality | stability |
160
+ * |------|---------|-----------|
161
+ * | frontend | rare | experimental |
162
+ *
163
+ * Core UI components based on Mantine UI v8.
164
+ *
165
+ * **Features:**
166
+ * - Mantine integration with theme support
167
+ * - ActionButton, BurgerButton, ClipboardButton, DarkModeButton, LanguageButton, ThemeButton
168
+ * - AlertDialog, ConfirmDialog, PromptDialog
169
+ * - Form controls: Control, ControlArray, ControlDate, ControlNumber, ControlObject, ControlSelect, ControlQueryBuilder
170
+ * - TypeForm for automatic form generation from TypeBox schemas
171
+ * - AdminShell layout component
172
+ * - AppBar with configurable elements
173
+ * - Sidebar navigation with sections and menu items
174
+ * - Omnibar for command palette / search
175
+ * - DataTable with filtering, sorting, pagination
176
+ * - Toast notifications
177
+ * - Theme system with dark mode
156
178
  *
157
179
  * @module alepha.ui
158
180
  */
@@ -168,3 +190,32 @@ export const AlephaUI = $module({
168
190
  alepha.with(ToastService);
169
191
  },
170
192
  });
193
+
194
+ /**
195
+ * Register UI components and get the RootRouter instance.
196
+ */
197
+ export const $ui = (
198
+ opts: {
199
+ // TODO:
200
+ // theme?: ThemeOptions;
201
+ // root?: string = "/";
202
+ } = {},
203
+ ) => {
204
+ const { alepha } = $context();
205
+
206
+ // TODO: Register unique instance ? In order to have multiple ui apps in the same context ?
207
+ // app = $ui();
208
+ // admin = $ui({ root: "/admin", theme: adminTheme });
209
+ // auth = $ui({ root: "/auth", theme: authTheme });
210
+ // etc...
211
+
212
+ /**
213
+ * If multi ui, should we have N themes ? or one $atom theme but with change based on current ui app ?
214
+ *
215
+ * App (theme=T1) -> Admin (theme=T2) ?
216
+ *
217
+ * > It can be done with onLeave()/onEnter() of the RootRouter to set the theme atom.
218
+ */
219
+
220
+ return alepha.inject(RootRouter); // Inject as singleton ?
221
+ };
@@ -1,5 +1,5 @@
1
- import { $head } from "@alepha/react/head";
2
1
  import { $inject, Alepha, AlephaError } from "alepha";
2
+ import { $head } from "alepha/react/head";
3
3
  import { $cookie } from "alepha/server/cookies";
4
4
  import { alephaThemeAtom } from "../atoms/alephaThemeAtom.ts";
5
5
  import { alephaThemeListAtom } from "../atoms/alephaThemeListAtom.ts";