@alepha/ui 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CF_qOO3u.js} +20 -19
  2. package/dist/admin/AdminApiKeys-CF_qOO3u.js.map +1 -0
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BQno3hZG.js} +7 -7
  4. package/dist/admin/AdminAudits-BQno3hZG.js.map +1 -0
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-kvuUaASF.js} +3 -4
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-kvuUaASF.js.map} +1 -1
  7. package/dist/admin/AdminJobDashboard-CrPxp0W1.js +485 -0
  8. package/dist/admin/AdminJobDashboard-CrPxp0W1.js.map +1 -0
  9. package/dist/admin/AdminJobExecutions-D-b4Zt7W.js +678 -0
  10. package/dist/admin/AdminJobExecutions-D-b4Zt7W.js.map +1 -0
  11. package/dist/admin/AdminJobRegistry-CNX5cpDx.js +301 -0
  12. package/dist/admin/AdminJobRegistry-CNX5cpDx.js.map +1 -0
  13. package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-e-ZP5nWw.js} +6 -9
  14. package/dist/admin/AdminLayout-e-ZP5nWw.js.map +1 -0
  15. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-DeHJFf6W.js} +3 -4
  16. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-DeHJFf6W.js.map} +1 -1
  17. package/dist/admin/AdminParameters-iQE8o7a7.js +774 -0
  18. package/dist/admin/AdminParameters-iQE8o7a7.js.map +1 -0
  19. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-oKJCbd7w.js} +5 -6
  20. package/dist/admin/AdminSessions-oKJCbd7w.js.map +1 -0
  21. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-BNCEle_E.js} +6 -7
  22. package/dist/admin/AdminUserAudits-BNCEle_E.js.map +1 -0
  23. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CgqeFwCt.js} +6 -6
  24. package/dist/admin/AdminUserCreate-CgqeFwCt.js.map +1 -0
  25. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-DDe1A1GP.js} +30 -28
  26. package/dist/admin/AdminUserDetails-DDe1A1GP.js.map +1 -0
  27. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-HAlobhWf.js} +20 -19
  28. package/dist/admin/AdminUserLayout-HAlobhWf.js.map +1 -0
  29. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Bq1LnVLf.js} +5 -6
  30. package/dist/admin/AdminUserSessions-Bq1LnVLf.js.map +1 -0
  31. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-BRsBZoxV.js} +10 -9
  32. package/dist/admin/AdminUserSettings-BRsBZoxV.js.map +1 -0
  33. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-D71kIOSn.js} +6 -7
  34. package/dist/admin/AdminUsers-D71kIOSn.js.map +1 -0
  35. package/dist/admin/index.d.ts +21 -85
  36. package/dist/admin/index.d.ts.map +1 -1
  37. package/dist/admin/index.js +66 -88
  38. package/dist/admin/index.js.map +1 -1
  39. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
  40. package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
  41. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  42. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  43. package/dist/auth/{Login-BBqTosqZ.js → Login-BS_FYTy0.js} +19 -13
  44. package/dist/auth/Login-BS_FYTy0.js.map +1 -0
  45. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-CjDsW378.js} +17 -12
  46. package/dist/auth/Profile-CjDsW378.js.map +1 -0
  47. package/dist/auth/{Register-Ce675Crg.js → Register-C5eqzAaD.js} +27 -17
  48. package/dist/auth/Register-C5eqzAaD.js.map +1 -0
  49. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-XifinVao.js} +17 -10
  50. package/dist/auth/ResetPassword-XifinVao.js.map +1 -0
  51. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DTgbeJOO.js} +9 -6
  52. package/dist/auth/VerifyEmail-DTgbeJOO.js.map +1 -0
  53. package/dist/auth/index.d.ts +18 -14
  54. package/dist/auth/index.d.ts.map +1 -1
  55. package/dist/auth/index.js +19 -18
  56. package/dist/auth/index.js.map +1 -1
  57. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  58. package/dist/core/index.d.ts +182 -92
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/core/index.js +789 -476
  61. package/dist/core/index.js.map +1 -1
  62. package/dist/demo/DemoDataTable-lnBKWBf8.js +362 -0
  63. package/dist/demo/DemoDataTable-lnBKWBf8.js.map +1 -0
  64. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-CUMZsYaH.js} +6 -6
  65. package/dist/demo/DemoHome-CUMZsYaH.js.map +1 -0
  66. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-_uokbGaW.js} +18 -19
  67. package/dist/demo/DemoJsonViewer-_uokbGaW.js.map +1 -0
  68. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-DHVoacE6.js} +4 -5
  69. package/dist/demo/DemoLayout-DHVoacE6.js.map +1 -0
  70. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DjJ9314c.js} +27 -24
  71. package/dist/demo/DemoLogin-DjJ9314c.js.map +1 -0
  72. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DzkJ5M83.js} +39 -32
  73. package/dist/demo/DemoRegister-DzkJ5M83.js.map +1 -0
  74. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-DWh4_BpQ.js} +30 -26
  75. package/dist/demo/DemoResetPassword-DWh4_BpQ.js.map +1 -0
  76. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-C1csnGhX.js} +4 -5
  77. package/dist/demo/DemoSidebar-C1csnGhX.js.map +1 -0
  78. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-CWz6fJrJ.js} +4 -5
  79. package/dist/demo/DemoTypeForm-CWz6fJrJ.js.map +1 -0
  80. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-DbU_tCj8.js} +16 -16
  81. package/dist/demo/DemoVerifyEmail-DbU_tCj8.js.map +1 -0
  82. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-Ch1m3Uzl.js} +2 -4
  83. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-Ch1m3Uzl.js.map} +1 -1
  84. package/dist/demo/{Showcase-CQrMWars.js → Showcase-BzoXNlCn.js} +11 -13
  85. package/dist/demo/Showcase-BzoXNlCn.js.map +1 -0
  86. package/dist/demo/index.d.ts +3 -70
  87. package/dist/demo/index.d.ts.map +1 -1
  88. package/dist/demo/index.js +11 -15
  89. package/dist/demo/index.js.map +1 -1
  90. package/dist/json/index.js +2 -2
  91. package/dist/json/index.js.map +1 -1
  92. package/package.json +11 -5
  93. package/src/admin/AdminRouter.ts +51 -29
  94. package/src/admin/components/AdminLayout.tsx +6 -9
  95. package/src/admin/components/audits/AdminAudits.tsx +5 -5
  96. package/src/admin/components/jobs/AdminJobDashboard.tsx +455 -0
  97. package/src/admin/components/jobs/AdminJobExecutions.tsx +693 -0
  98. package/src/admin/components/jobs/AdminJobRegistry.tsx +325 -0
  99. package/src/admin/components/keys/AdminApiKeys.tsx +28 -31
  100. package/src/admin/components/parameters/AdminParameters.tsx +156 -78
  101. package/src/admin/components/parameters/ParameterDetails.tsx +173 -108
  102. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  103. package/src/admin/components/parameters/ParameterHistory.tsx +22 -35
  104. package/src/admin/components/parameters/ParameterTree.tsx +283 -109
  105. package/src/admin/components/parameters/types.ts +3 -3
  106. package/src/admin/components/sessions/AdminSessions.tsx +3 -3
  107. package/src/admin/components/shared/AdminResourceHeader.tsx +20 -16
  108. package/src/admin/components/users/AdminUserAudits.tsx +5 -5
  109. package/src/admin/components/users/AdminUserCreate.tsx +3 -3
  110. package/src/admin/components/users/AdminUserDetails.tsx +51 -53
  111. package/src/admin/components/users/AdminUserLayout.tsx +7 -7
  112. package/src/admin/components/users/AdminUserSessions.tsx +3 -3
  113. package/src/admin/components/users/AdminUserSettings.tsx +9 -9
  114. package/src/admin/components/users/AdminUsers.tsx +5 -5
  115. package/src/admin/components/verifications/AdminVerifications.tsx +3 -3
  116. package/src/admin/index.ts +0 -24
  117. package/src/admin/primitives/$uiAdmin.ts +2 -2
  118. package/src/auth/AuthRouter.ts +1 -0
  119. package/src/auth/components/Login.tsx +13 -13
  120. package/src/auth/components/Profile.tsx +17 -26
  121. package/src/auth/components/Register.tsx +21 -31
  122. package/src/auth/components/ResetPassword.tsx +13 -22
  123. package/src/auth/components/VerifyEmail.tsx +5 -5
  124. package/src/auth/components/buttons/UserButton.tsx +14 -4
  125. package/src/core/components/buttons/ActionButton.tsx +13 -17
  126. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  127. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  128. package/src/core/components/data/ErrorViewer.tsx +15 -15
  129. package/src/core/components/dialogs/AlertDialog.tsx +3 -3
  130. package/src/core/components/dialogs/ConfirmDialog.tsx +3 -3
  131. package/src/core/components/dialogs/PromptDialog.tsx +3 -3
  132. package/src/core/components/form/Control.tsx +19 -32
  133. package/src/core/components/form/ControlArray.tsx +206 -96
  134. package/src/core/components/form/ControlObject.tsx +3 -3
  135. package/src/core/components/form/ControlQueryBuilder.tsx +20 -22
  136. package/src/core/components/form/ControlSelect.tsx +4 -0
  137. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  138. package/src/core/components/form/TypeForm.tsx +7 -0
  139. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  140. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  141. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  142. package/src/core/components/layout/Omnibar.tsx +2 -1
  143. package/src/core/components/layout/Sidebar.tsx +63 -19
  144. package/src/core/components/table/ColumnPicker.tsx +47 -31
  145. package/src/core/components/table/DataTable.tsx +277 -201
  146. package/src/core/components/table/DataTableFilters.tsx +8 -0
  147. package/src/core/components/table/DataTableToolbar.tsx +98 -5
  148. package/src/core/components/table/FilterPicker.tsx +28 -26
  149. package/src/core/components/table/types.ts +52 -37
  150. package/src/core/components/table/useTableSelection.ts +83 -0
  151. package/src/core/constants/ui.ts +1 -1
  152. package/src/core/helpers/renderIcon.tsx +5 -2
  153. package/src/core/index.ts +9 -5
  154. package/src/core/styles.css +8 -7
  155. package/src/core/utils/parseInput.ts +1 -0
  156. package/src/core/utils/string.ts +28 -4
  157. package/src/demo/components/DemoHome.tsx +5 -5
  158. package/src/demo/components/DemoLayout.tsx +6 -2
  159. package/src/demo/components/core/DemoDataTable.tsx +209 -5
  160. package/src/demo/components/json/DemoJsonViewer.tsx +1 -1
  161. package/src/demo/components/shared/MacWindow.tsx +7 -7
  162. package/src/demo/components/shared/Showcase.tsx +3 -3
  163. package/src/demo/index.ts +0 -11
  164. package/src/json/components/JsonViewer.tsx +3 -3
  165. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  166. package/dist/admin/AdminApiKeys-GMORg-1l.js.map +0 -1
  167. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  168. package/dist/admin/AdminAudits-pkWrjq1Z.js.map +0 -1
  169. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  170. package/dist/admin/AdminJobs-B-q9iGO3.js +0 -697
  171. package/dist/admin/AdminJobs-B-q9iGO3.js.map +0 -1
  172. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  173. package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
  174. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  175. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  176. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  177. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  178. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  179. package/dist/admin/AdminSessions-DzIOxM3b.js.map +0 -1
  180. package/dist/admin/AdminUserAudits-CiUPN2BC.js.map +0 -1
  181. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  182. package/dist/admin/AdminUserCreate-BwQKr4xE.js.map +0 -1
  183. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  184. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  185. package/dist/admin/AdminUserDetails-uqtC5aJ1.js.map +0 -1
  186. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  187. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  188. package/dist/admin/AdminUserSessions-DAE8Nf1F.js.map +0 -1
  189. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  190. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  191. package/dist/admin/AdminUserSettings-EbahaV2a.js.map +0 -1
  192. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  193. package/dist/admin/AdminUsers-Dcjh0KNW.js.map +0 -1
  194. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  195. package/dist/auth/Login-CoU63mMR.js +0 -4
  196. package/dist/auth/Profile-Bxj8Nwom.js.map +0 -1
  197. package/dist/auth/Register-BV_oa_AK.js +0 -4
  198. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  199. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  200. package/dist/auth/ResetPassword-DWdt7c40.js.map +0 -1
  201. package/dist/auth/VerifyEmail-CI4JwByV.js.map +0 -1
  202. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  203. package/dist/demo/DemoDataTable-CguplbR7.js +0 -150
  204. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  205. package/dist/demo/DemoHome-Cce2bWmg.js.map +0 -1
  206. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  207. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  208. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  209. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  210. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  211. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  212. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  213. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  214. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  215. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  216. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  217. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
  218. package/src/admin/components/jobs/AdminJobs.tsx +0 -772
@@ -39,6 +39,8 @@ export interface TypeFormProps<T extends TObject> {
39
39
 
40
40
  fill?: boolean;
41
41
  flexProps?: FlexProps;
42
+
43
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
42
44
  }
43
45
 
44
46
  /**
@@ -93,6 +95,7 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
93
95
  skipSubmitButton = false,
94
96
  submitButtonProps,
95
97
  fill = true,
98
+ size,
96
99
  } = props;
97
100
 
98
101
  const schema = props.schema || form.options.schema;
@@ -163,6 +166,10 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
163
166
  ...fieldControlProps?.[fieldName],
164
167
  };
165
168
 
169
+ if (size) {
170
+ mergedControlProps.size = size;
171
+ }
172
+
166
173
  return (
167
174
  <Grid.Col key={fieldName} span={span}>
168
175
  <Control input={field} {...mergedControlProps} />
@@ -72,6 +72,7 @@ const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
72
72
  {...props.mantine}
73
73
  defaultColorScheme={defaultColorScheme}
74
74
  theme={{
75
+ cursorType: "pointer",
75
76
  // Spread all theme properties from the selected theme
76
77
  ...theme,
77
78
  // User overrides take precedence
@@ -0,0 +1,91 @@
1
+ import { Anchor, Flex, type FlexProps, Text } from "@mantine/core";
2
+ import { IconChevronRight } from "@tabler/icons-react";
3
+ import { Link, useRouter, useRouterState } from "alepha/react/router";
4
+ import type { ReactNode } from "react";
5
+ import { toTitleCase } from "../../utils/string.ts";
6
+
7
+ export interface BreadcrumbProps extends FlexProps {
8
+ /**
9
+ * Label for the home/root crumb. Set to `false` to hide the root crumb.
10
+ *
11
+ * @default "Home"
12
+ */
13
+ home?: string | false;
14
+
15
+ /**
16
+ * Custom separator between crumbs.
17
+ *
18
+ * @default IconChevronRight
19
+ */
20
+ separator?: ReactNode;
21
+
22
+ /**
23
+ * Size of text and separator icons.
24
+ *
25
+ * @default "xs"
26
+ */
27
+ size?: string;
28
+ }
29
+
30
+ /**
31
+ * Automatic breadcrumb component that reads the current route hierarchy
32
+ * from the Alepha router's layer stack.
33
+ *
34
+ * Pages should define a `label` in their `$page()` options for best results.
35
+ * Falls back to the page name converted to Title Case.
36
+ */
37
+ const Breadcrumb = ({
38
+ home = "Home",
39
+ separator,
40
+ size = "xs",
41
+ ...groupProps
42
+ }: BreadcrumbProps) => {
43
+ const state = useRouterState();
44
+ const router = useRouter();
45
+
46
+ const crumbs: Array<{ label: string; href: string }> = [];
47
+
48
+ // Optionally add home crumb
49
+ if (home !== false) {
50
+ crumbs.push({ label: home, href: "/" });
51
+ }
52
+
53
+ // Build crumbs from layers, skipping the root layout (index 0)
54
+ for (let i = 1; i < state.layers.length; i++) {
55
+ const layer = state.layers[i];
56
+ const route = layer.route as any;
57
+
58
+ // Skip index routes (path "/") — they share the parent URL
59
+ if (route?.path === "/" || route?.path === "") continue;
60
+
61
+ const label = route?.label ?? toTitleCase(layer.name);
62
+ // Use router.path() to resolve dynamic params (e.g. :userId → 3)
63
+ const href = router.path(layer.name);
64
+ crumbs.push({ label, href });
65
+ }
66
+
67
+ if (crumbs.length === 0) return null;
68
+
69
+ const sep = separator ?? <IconChevronRight size={12} color="#9ca3af" />;
70
+
71
+ return (
72
+ <Flex gap={4} {...groupProps}>
73
+ {crumbs.map((crumb, i) => (
74
+ <Flex key={crumb.href} gap={4}>
75
+ {i > 0 && sep}
76
+ {i < crumbs.length - 1 ? (
77
+ <Anchor component={Link} href={crumb.href} size={size} c="dimmed">
78
+ {crumb.label}
79
+ </Anchor>
80
+ ) : (
81
+ <Text size={size} fw={500}>
82
+ {crumb.label}
83
+ </Text>
84
+ )}
85
+ </Flex>
86
+ ))}
87
+ </Flex>
88
+ );
89
+ };
90
+
91
+ export default Breadcrumb;
@@ -19,7 +19,6 @@ import {
19
19
  useState,
20
20
  } from "react";
21
21
  import { alephaSidebarAtom } from "../../atoms/alephaSidebarAtom.ts";
22
- import { ui } from "../../constants/ui.ts";
23
22
  import AppBar, { type AppBarProps } from "./AppBar.tsx";
24
23
  import {
25
24
  Sidebar,
@@ -27,7 +26,7 @@ import {
27
26
  type SidebarProps,
28
27
  } from "./Sidebar.tsx";
29
28
 
30
- export interface AdminShellProps {
29
+ export interface DashboardShellProps {
31
30
  appShellProps?: Partial<AppShellProps>;
32
31
  appShellMainProps?: Partial<AppShellMainProps>;
33
32
  appShellHeaderProps?: Partial<AppShellHeaderProps>;
@@ -39,6 +38,35 @@ export interface AdminShellProps {
39
38
  footer?: ReactNode;
40
39
  children?: ReactNode;
41
40
 
41
+ /**
42
+ * AppShell layout mode.
43
+ * - "default": header/footer span full width, navbar below header.
44
+ * - "alt": navbar is full height, header/footer offset by navbar width.
45
+ */
46
+ layout?: "default" | "alt";
47
+
48
+ /**
49
+ * Content rendered above the Sidebar inside the navbar (e.g. logo).
50
+ */
51
+ navbarHeader?: ReactNode;
52
+
53
+ /**
54
+ * Content rendered below the Sidebar inside the navbar (e.g. toggle button).
55
+ */
56
+ navbarFooter?: ReactNode;
57
+
58
+ /**
59
+ * Height of the header bar in pixels.
60
+ * @default 60
61
+ */
62
+ headerHeight?: number;
63
+
64
+ /**
65
+ * Height of the footer bar in pixels.
66
+ * @default 24
67
+ */
68
+ footerHeight?: number;
69
+
42
70
  /**
43
71
  * Enable drag-to-resize for the sidebar.
44
72
  * Width and constraints are configured in alephaSidebarAtom.
@@ -59,17 +87,14 @@ export interface AdminShellProps {
59
87
  container?: boolean | ContainerProps;
60
88
  }
61
89
 
62
- const AdminShell = (props: AdminShellProps) => {
90
+ const DashboardShell = (props: DashboardShellProps) => {
63
91
  const router = useRouter();
64
92
  const [sidebar, setSidebar] = useStore(alephaSidebarAtom);
65
- const { opened, collapsed } = sidebar;
66
-
67
- // Initialize collapsed state from props on mount
68
- useEffect(() => {
69
- if (props.sidebarProps?.collapsed !== undefined) {
70
- setSidebar({ ...sidebar, collapsed: props.sidebarProps.collapsed });
71
- }
72
- }, []);
93
+ const { opened } = sidebar;
94
+ const collapsed =
95
+ props.sidebarProps?.collapsed !== undefined
96
+ ? props.sidebarProps.collapsed
97
+ : sidebar.collapsed;
73
98
 
74
99
  // Resize state
75
100
  const [isResizing, setIsResizing] = useState(false);
@@ -169,13 +194,15 @@ const AdminShell = (props: AdminShellProps) => {
169
194
  // Hover to expand when collapsed (with delay)
170
195
  const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
171
196
 
197
+ const expandOnHover = props.sidebarProps?.expandOnHover !== false;
198
+
172
199
  const handleNavbarMouseEnter = useCallback(() => {
173
- if (collapsed) {
200
+ if (collapsed && expandOnHover) {
174
201
  hoverTimeoutRef.current = setTimeout(() => {
175
202
  setIsHovering(true);
176
203
  }, hoverDelay);
177
204
  }
178
- }, [collapsed, hoverDelay]);
205
+ }, [collapsed, expandOnHover, hoverDelay]);
179
206
 
180
207
  const handleNavbarMouseLeave = useCallback(() => {
181
208
  if (hoverTimeoutRef.current) {
@@ -235,10 +262,12 @@ const AdminShell = (props: AdminShellProps) => {
235
262
  appBarProps.container ??= props.container;
236
263
 
237
264
  const hasSidebar = showSidebar && props.sidebarProps !== undefined;
238
- const hasAppBar = hasSidebar || props.appBarProps || props.header;
265
+ const hasAppBar = props.appBarProps || props.header;
239
266
 
240
- const headerHeight = hasAppBar ? 60 : 0;
241
- const footerHeight = props.footer ? 24 : 0;
267
+ const hHeight = props.headerHeight ?? 60;
268
+ const fHeight = props.footerHeight ?? 24;
269
+ const headerHeight = hasAppBar ? hHeight : 0;
270
+ const footerHeight = props.footer ? fHeight : 0;
242
271
  const expandedWidth = Math.max(sidebar.width, collapsedWidth);
243
272
 
244
273
  // When collapsed but hovering, show defaultWidth (not current width)
@@ -266,10 +295,10 @@ const AdminShell = (props: AdminShellProps) => {
266
295
 
267
296
  return (
268
297
  <AppShell
298
+ layout={props.layout}
269
299
  w={"100%"}
270
300
  flex={1}
271
- padding="md"
272
- header={hasAppBar ? { height: 60 } : undefined}
301
+ header={hasAppBar ? { height: hHeight } : undefined}
273
302
  navbar={
274
303
  hasSidebar
275
304
  ? {
@@ -283,16 +312,19 @@ const AdminShell = (props: AdminShellProps) => {
283
312
  }
284
313
  : undefined
285
314
  }
286
- footer={props.footer ? { height: 24 } : undefined}
315
+ footer={props.footer ? { height: fHeight } : undefined}
287
316
  {...props.appShellProps}
288
317
  >
289
- <AppShell.Header bg={ui.colors.surface} {...props.appShellHeaderProps}>
290
- {props.header ?? <AppBar items={defaultAppBarItems} {...appBarProps} />}
291
- </AppShell.Header>
318
+ {hasAppBar && (
319
+ <AppShell.Header {...props.appShellHeaderProps}>
320
+ {props.header ?? (
321
+ <AppBar items={defaultAppBarItems} {...appBarProps} />
322
+ )}
323
+ </AppShell.Header>
324
+ )}
292
325
 
293
326
  {hasSidebar && (
294
327
  <AppShell.Navbar
295
- bg={ui.colors.surface}
296
328
  className="alepha-sidebar-navbar"
297
329
  data-resizing={isResizing}
298
330
  data-hover-expanded={isExpandedByHover}
@@ -312,11 +344,31 @@ const AdminShell = (props: AdminShellProps) => {
312
344
  }}
313
345
  {...props.appShellNavbarProps}
314
346
  >
347
+ {props.navbarHeader ? (
348
+ <Flex
349
+ style={{
350
+ borderBottom: "1px solid var(--mantine-color-default-border)",
351
+ }}
352
+ h={headerHeight}
353
+ >
354
+ {props.navbarHeader}
355
+ </Flex>
356
+ ) : null}
315
357
  <Sidebar
316
358
  {...(props.sidebarProps ?? {})}
317
359
  collapsed={effectiveCollapsed}
318
360
  onItemClick={handleSidebarItemClick}
319
361
  />
362
+ {props.navbarFooter ? (
363
+ <Flex
364
+ style={{
365
+ borderTop: "1px solid var(--mantine-color-default-border)",
366
+ }}
367
+ h={footerHeight}
368
+ >
369
+ {props.navbarFooter}
370
+ </Flex>
371
+ ) : null}
320
372
  {(canResize || isExpandedByHover) && (
321
373
  <Flex
322
374
  pos="absolute"
@@ -335,13 +387,6 @@ const AdminShell = (props: AdminShellProps) => {
335
387
  )}
336
388
 
337
389
  <AppShell.Main
338
- pl={sidebarWidth}
339
- pt={headerHeight}
340
- pb={footerHeight}
341
- pr={0}
342
- display={"flex"}
343
- flex={1}
344
- style={{ flexDirection: "column" }}
345
390
  className="alepha-sidebar-main"
346
391
  data-resizing={isResizing}
347
392
  {...props.appShellMainProps}
@@ -362,7 +407,7 @@ const AdminShell = (props: AdminShellProps) => {
362
407
  </AppShell.Main>
363
408
 
364
409
  {props.footer && (
365
- <AppShell.Footer bg={ui.colors.surface} {...props.appShellFooterProps}>
410
+ <AppShell.Footer {...props.appShellFooterProps}>
366
411
  {props.footer}
367
412
  </AppShell.Footer>
368
413
  )}
@@ -370,4 +415,4 @@ const AdminShell = (props: AdminShellProps) => {
370
415
  );
371
416
  };
372
417
 
373
- export default AdminShell;
418
+ export default DashboardShell;
@@ -2,6 +2,7 @@ import { Spotlight, type SpotlightActionData } from "@mantine/spotlight";
2
2
  import { IconSearch } from "@tabler/icons-react";
3
3
  import { useStore } from "alepha/react";
4
4
  import { useRouter } from "alepha/react/router";
5
+ import { currentUserAtom } from "alepha/security";
5
6
  import { type ReactNode, useMemo } from "react";
6
7
  import { ui } from "../../constants/ui.ts";
7
8
  import { renderIcon } from "../../helpers/renderIcon.tsx";
@@ -19,7 +20,7 @@ const Omnibar = (props: OmnibarProps) => {
19
20
  const router = useRouter();
20
21
 
21
22
  // watch user to re-render on permission changes
22
- const [user] = useStore("alepha.server.request.user");
23
+ const [user] = useStore(currentUserAtom);
23
24
 
24
25
  const actions: SpotlightActionData[] = useMemo(
25
26
  () =>
@@ -13,11 +13,13 @@ import { useEvents } from "alepha/react";
13
13
  import { useRouter } from "alepha/react/router";
14
14
  import {
15
15
  type ComponentType,
16
+ Fragment,
16
17
  type ReactNode,
17
18
  useCallback,
18
19
  useMemo,
19
20
  useState,
20
21
  } from "react";
22
+ import { ui } from "../../constants/ui.ts";
21
23
  import { renderIcon } from "../../helpers/renderIcon.tsx";
22
24
  import ActionButton, { type ActionProps } from "../buttons/ActionButton.tsx";
23
25
  import OmnibarButton from "../buttons/OmnibarButton.tsx";
@@ -35,6 +37,12 @@ export interface SidebarProps {
35
37
  paths?: string[];
36
38
  };
37
39
 
40
+ /**
41
+ * Whether the sidebar expands on hover when collapsed.
42
+ * @default true
43
+ */
44
+ expandOnHover?: boolean;
45
+
38
46
  /**
39
47
  * Automatically populate the menu from the router's pages.
40
48
  */
@@ -49,19 +57,25 @@ export const Sidebar = (props: SidebarProps) => {
49
57
  const router = useRouter();
50
58
  const { onItemClick } = props;
51
59
 
52
- const divider = (key: string | number) => {
60
+ const divider = (key: string | number, fill?: boolean) => {
53
61
  return (
54
62
  <Flex
55
63
  key={key}
56
64
  h={1}
57
- bg={"var(--alepha-border)"}
65
+ bg={"var(--mantine-color-default-border)"}
58
66
  my={"xs"}
59
- mx={props.collapsed ? 0 : "sm"}
67
+ mx={
68
+ fill
69
+ ? "calc(-1 * var(--mantine-spacing-md))"
70
+ : props.collapsed
71
+ ? 0
72
+ : "sm"
73
+ }
60
74
  />
61
75
  );
62
76
  };
63
77
 
64
- const renderNode = (item: SidebarNode, key: number) => {
78
+ const renderNode = (item: SidebarNode, key: number | string) => {
65
79
  if ("type" in item) {
66
80
  // Hide spacers when collapsed
67
81
  if (item.type === "spacer") {
@@ -70,7 +84,7 @@ export const Sidebar = (props: SidebarProps) => {
70
84
  }
71
85
 
72
86
  if (item.type === "divider") {
73
- return divider(key);
87
+ return divider(key, item.fill);
74
88
  }
75
89
 
76
90
  if (item.type === "search") {
@@ -85,24 +99,45 @@ export const Sidebar = (props: SidebarProps) => {
85
99
  return <ToggleSidebarButton key={key} />;
86
100
  }
87
101
 
102
+ // Replace sections with dividers when collapsed
88
103
  // Replace sections with dividers when collapsed
89
104
  if (item.type === "section") {
105
+ // Hide section if all children are hidden
106
+ if (item.children && item.children.length > 0) {
107
+ const hasVisibleChild = item.children.some(
108
+ (child) => !("can" in child) || !child.can || child.can(),
109
+ );
110
+ if (!hasVisibleChild) return null;
111
+ }
112
+
90
113
  if (props.collapsed) {
91
- return divider(key);
114
+ return (
115
+ <Fragment key={key}>
116
+ {divider(`${key}-d`)}
117
+ {item.children?.map((child, index) =>
118
+ renderNode(child, `s${key}-${index}`),
119
+ )}
120
+ </Fragment>
121
+ );
92
122
  }
93
123
  return (
94
- <Flex key={key} mt={"md"} align={"center"} gap={"xs"}>
95
- {renderIcon(item.icon)}
96
- <Text size={"xs"} c={"dimmed"} tt={"uppercase"} fw={"bold"}>
97
- {item.label}
98
- </Text>
99
- </Flex>
124
+ <Fragment key={key}>
125
+ <Flex mt={"md"} align={"center"} gap={"xs"}>
126
+ {renderIcon(item.icon, ui.sizes.icon.sm)}
127
+ <Text size={"xs"} c={"dimmed"} tt={"uppercase"} fw={"bold"}>
128
+ {item.label}
129
+ </Text>
130
+ </Flex>
131
+ {item.children?.map((child, index) =>
132
+ renderNode(child, `s${key}-${index}`),
133
+ )}
134
+ </Fragment>
100
135
  );
101
136
  }
102
137
  }
103
138
 
104
139
  if ("element" in item) {
105
- return <Flex key={key}>{item.element}</Flex>;
140
+ return <Fragment key={key}>{item.element}</Fragment>;
106
141
  }
107
142
 
108
143
  // Check visibility control
@@ -167,7 +202,7 @@ export const Sidebar = (props: SidebarProps) => {
167
202
  };
168
203
 
169
204
  const padding = "md";
170
- const gap = props.items ? (props.gap ?? 2) : "xs";
205
+ const gap = props.items ? (props.gap ?? 4) : "xs";
171
206
  const menu = useMemo(
172
207
  () => getSidebarNodes(),
173
208
  [props.items, props.autoPopulateMenu],
@@ -275,8 +310,11 @@ export const SidebarItem = (props: SidebarItemProps) => {
275
310
  props.theme.button?.size ??
276
311
  (level === 0 ? "sm" : "xs")
277
312
  }
278
- tooltip={item.description}
279
- c={"var(--mantine-color-text)"}
313
+ tooltip={
314
+ item.description
315
+ ? { label: item.description, position: "right" }
316
+ : undefined
317
+ }
280
318
  color={"gray"}
281
319
  variant={"subtle"}
282
320
  variantActive={"default"}
@@ -284,7 +322,7 @@ export const SidebarItem = (props: SidebarItemProps) => {
284
322
  onClick={handleItemClick}
285
323
  leftSection={
286
324
  <Flex w={"100%"} align="center" gap={"sm"}>
287
- {renderIcon(item.icon)}
325
+ {renderIcon(item.icon, ui.sizes.icon.sm)}
288
326
  <Flex direction={"column"}>
289
327
  <Flex>{item.label}</Flex>
290
328
  </Flex>
@@ -313,7 +351,7 @@ export const SidebarItem = (props: SidebarItemProps) => {
313
351
  position: "absolute",
314
352
  width: 1,
315
353
  background:
316
- "linear-gradient(to bottom, transparent, var(--alepha-border), transparent)",
354
+ "linear-gradient(to bottom, transparent, var(--mantine-color-default-border), transparent)",
317
355
  top: 48,
318
356
  left: 20 + 32 * level,
319
357
  bottom: 16,
@@ -368,7 +406,11 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
368
406
  }}
369
407
  radius={props.item.theme?.radius ?? props.theme.button?.radius ?? "md"}
370
408
  onClick={handleItemClick}
371
- icon={renderIcon(item.icon) ?? <IconSquareRounded />}
409
+ icon={
410
+ renderIcon(item.icon, ui.sizes.icon.sm) ?? (
411
+ <IconSquareRounded size={ui.sizes.icon.sm} />
412
+ )
413
+ }
372
414
  href={props.item.href as any}
373
415
  target={props.item.target}
374
416
  {...props.item.actionProps}
@@ -401,6 +443,7 @@ export interface SidebarSpacer extends SidebarAbstractItem {
401
443
 
402
444
  export interface SidebarDivider extends SidebarAbstractItem {
403
445
  type: "divider";
446
+ fill?: true;
404
447
  }
405
448
 
406
449
  export interface SidebarSearch extends SidebarAbstractItem {
@@ -415,6 +458,7 @@ export interface SidebarSection extends SidebarAbstractItem {
415
458
  type: "section";
416
459
  label: string;
417
460
  icon?: ReactNode | ComponentType;
461
+ children?: SidebarNode[];
418
462
  }
419
463
 
420
464
  export interface SidebarMenuItem extends SidebarAbstractItem {
@@ -1,18 +1,14 @@
1
- import {
2
- Button,
3
- Checkbox,
4
- Group,
5
- Popover,
6
- ScrollArea,
7
- Stack,
8
- Text,
9
- } from "@mantine/core";
1
+ import { Checkbox, Flex, Popover, ScrollArea, Text } from "@mantine/core";
10
2
  import { IconColumns } from "@tabler/icons-react";
11
3
  import type { TObject } from "alepha";
12
4
  import { useState } from "react";
13
5
  import { ui } from "../../constants/ui.ts";
14
6
  import ActionButton from "../buttons/ActionButton.tsx";
15
- import type { ColumnVisibility, DataTableColumn } from "./types.ts";
7
+ import {
8
+ type ColumnVisibility,
9
+ type DataTableColumn,
10
+ DEFAULT_MAX_VISIBLE_COLUMNS,
11
+ } from "./types.ts";
16
12
 
17
13
  export interface ColumnPickerProps<T extends object, Filters extends TObject> {
18
14
  columns: { [key: string]: DataTableColumn<T, Filters> };
@@ -36,11 +32,19 @@ const ColumnPicker = <T extends object, Filters extends TObject>({
36
32
  onVisibilityChange(newVisibility);
37
33
  };
38
34
 
39
- const handleHideAll = () => {
40
- const newVisibility = columnEntries.reduce(
41
- (acc, [key]) => ({ ...acc, [key]: false }),
42
- {} as ColumnVisibility,
43
- );
35
+ const handleDefault = () => {
36
+ let count = 0;
37
+ const newVisibility = columnEntries.reduce((acc, [key, col]) => {
38
+ if (col.defaultHidden) {
39
+ acc[key] = false;
40
+ } else if (count < DEFAULT_MAX_VISIBLE_COLUMNS) {
41
+ acc[key] = true;
42
+ count++;
43
+ } else {
44
+ acc[key] = false;
45
+ }
46
+ return acc;
47
+ }, {} as ColumnVisibility);
44
48
  onVisibilityChange(newVisibility);
45
49
  };
46
50
 
@@ -71,7 +75,13 @@ const ColumnPicker = <T extends object, Filters extends TObject>({
71
75
  }}
72
76
  >
73
77
  <Popover.Target>
74
- <ActionButton variant="subtle" icon={IconColumns} />
78
+ <div>
79
+ <ActionButton
80
+ variant="subtle"
81
+ icon={IconColumns}
82
+ onClick={() => setOpened((o) => !o)}
83
+ />
84
+ </div>
75
85
  </Popover.Target>
76
86
  <Popover.Dropdown
77
87
  bg="transparent"
@@ -81,43 +91,49 @@ const ColumnPicker = <T extends object, Filters extends TObject>({
81
91
  backdropFilter: "blur(20px)",
82
92
  }}
83
93
  >
84
- <Stack gap="xs" bg={ui.colors.surface} p="sm" bdrs="sm">
85
- <Group justify="space-between">
94
+ <Flex
95
+ direction="column"
96
+ gap="xs"
97
+ bg={ui.colors.surface}
98
+ p="sm"
99
+ bdrs="sm"
100
+ >
101
+ <Flex justify="space-between">
86
102
  <Text size="sm" fw={500}>
87
103
  Columns ({visibleCount}/{columnEntries.length})
88
104
  </Text>
89
- <Group gap={4}>
90
- <Button
105
+ <Flex gap={4}>
106
+ <ActionButton
91
107
  size="compact-xs"
92
108
  variant="subtle"
93
109
  onClick={handleShowAll}
94
110
  >
95
111
  All
96
- </Button>
97
- <Button
112
+ </ActionButton>
113
+ <ActionButton
98
114
  size="compact-xs"
99
115
  variant="subtle"
100
- onClick={handleHideAll}
116
+ onClick={handleDefault}
101
117
  >
102
- None
103
- </Button>
104
- </Group>
105
- </Group>
118
+ Default
119
+ </ActionButton>
120
+ </Flex>
121
+ </Flex>
106
122
 
107
123
  <ScrollArea.Autosize mah={300}>
108
- <Stack gap={4}>
124
+ <Flex direction="column" gap={4}>
109
125
  {columnEntries.map(([key, col]) => (
110
126
  <Checkbox
111
127
  key={key}
112
- label={col.label}
128
+ label={col.label || key}
113
129
  checked={visibility[key] !== false}
114
130
  onChange={(e) => handleToggle(key, e.currentTarget.checked)}
115
131
  size="sm"
116
132
  />
117
133
  ))}
118
- </Stack>
134
+ </Flex>
119
135
  </ScrollArea.Autosize>
120
- </Stack>
136
+ </Flex>
121
137
  </Popover.Dropdown>
122
138
  </Popover>
123
139
  );