@alepha/ui 0.16.1 → 0.16.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 (150) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
  2. package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
  4. package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
  7. package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
  8. package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
  9. package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-CsjvpeD1.js} +6 -9
  10. package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
  11. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
  12. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
  13. package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
  14. package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
  15. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
  16. package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
  17. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
  18. package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
  19. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
  20. package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
  21. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
  22. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
  23. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
  24. package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
  25. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
  26. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
  27. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
  28. package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
  29. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
  30. package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
  31. package/dist/admin/index.d.ts +22 -10
  32. package/dist/admin/index.d.ts.map +1 -1
  33. package/dist/admin/index.js +47 -48
  34. package/dist/admin/index.js.map +1 -1
  35. package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
  36. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
  37. package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
  38. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  39. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  40. package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
  41. package/dist/auth/Login-DS_OqA0G.js.map +1 -0
  42. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
  43. package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
  44. package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
  45. package/dist/auth/Register-BRR2_gux.js.map +1 -0
  46. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
  47. package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
  48. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
  49. package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
  50. package/dist/auth/index.d.ts +14 -14
  51. package/dist/auth/index.d.ts.map +1 -1
  52. package/dist/auth/index.js +13 -13
  53. package/dist/auth/index.js.map +1 -1
  54. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  55. package/dist/core/index.d.ts +147 -68
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +349 -287
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
  60. package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
  61. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
  62. package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
  63. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
  64. package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
  65. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
  66. package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
  67. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
  68. package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
  69. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
  70. package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
  71. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
  72. package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
  73. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
  74. package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
  75. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
  76. package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
  77. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
  78. package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
  79. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
  80. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
  81. package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
  82. package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
  83. package/dist/demo/index.d.ts +2 -2
  84. package/dist/demo/index.d.ts.map +1 -1
  85. package/dist/demo/index.js +15 -15
  86. package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
  87. package/package.json +5 -3
  88. package/src/admin/AdminRouter.ts +15 -24
  89. package/src/admin/components/AdminLayout.tsx +6 -9
  90. package/src/admin/components/parameters/AdminParameters.tsx +154 -76
  91. package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
  92. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  93. package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
  94. package/src/admin/components/parameters/ParameterTree.tsx +280 -104
  95. package/src/admin/components/parameters/types.ts +3 -3
  96. package/src/admin/primitives/$uiAdmin.ts +2 -2
  97. package/src/auth/AuthRouter.ts +1 -0
  98. package/src/core/components/buttons/ActionButton.tsx +4 -15
  99. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  100. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  101. package/src/core/components/form/Control.tsx +10 -32
  102. package/src/core/components/form/ControlArray.tsx +200 -89
  103. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  104. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  105. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  106. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  107. package/src/core/components/layout/Sidebar.tsx +58 -18
  108. package/src/core/constants/ui.ts +1 -1
  109. package/src/core/helpers/renderIcon.tsx +5 -2
  110. package/src/core/index.ts +9 -5
  111. package/src/core/styles.css +7 -7
  112. package/src/core/utils/string.ts +28 -4
  113. package/src/demo/components/DemoLayout.tsx +6 -2
  114. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  115. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  116. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  117. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  118. package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
  119. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  120. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  121. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  122. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  123. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  124. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  125. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  126. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  127. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  128. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  129. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  130. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  131. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  132. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  133. package/dist/auth/Login-CoU63mMR.js +0 -4
  134. package/dist/auth/Register-BV_oa_AK.js +0 -4
  135. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  136. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  137. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  138. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  139. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  140. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  141. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  142. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  143. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  144. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  145. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  146. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  147. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  148. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  149. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  150. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
@@ -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, Group, type GroupProps, 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 GroupProps {
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
+ <Group gap={4} {...groupProps}>
73
+ {crumbs.map((crumb, i) => (
74
+ <Group 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
+ </Group>
86
+ ))}
87
+ </Group>
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;
@@ -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],
@@ -276,7 +311,6 @@ export const SidebarItem = (props: SidebarItemProps) => {
276
311
  (level === 0 ? "sm" : "xs")
277
312
  }
278
313
  tooltip={item.description}
279
- c={"var(--mantine-color-text)"}
280
314
  color={"gray"}
281
315
  variant={"subtle"}
282
316
  variantActive={"default"}
@@ -284,7 +318,7 @@ export const SidebarItem = (props: SidebarItemProps) => {
284
318
  onClick={handleItemClick}
285
319
  leftSection={
286
320
  <Flex w={"100%"} align="center" gap={"sm"}>
287
- {renderIcon(item.icon)}
321
+ {renderIcon(item.icon, ui.sizes.icon.sm)}
288
322
  <Flex direction={"column"}>
289
323
  <Flex>{item.label}</Flex>
290
324
  </Flex>
@@ -313,7 +347,7 @@ export const SidebarItem = (props: SidebarItemProps) => {
313
347
  position: "absolute",
314
348
  width: 1,
315
349
  background:
316
- "linear-gradient(to bottom, transparent, var(--alepha-border), transparent)",
350
+ "linear-gradient(to bottom, transparent, var(--mantine-color-default-border), transparent)",
317
351
  top: 48,
318
352
  left: 20 + 32 * level,
319
353
  bottom: 16,
@@ -368,7 +402,11 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
368
402
  }}
369
403
  radius={props.item.theme?.radius ?? props.theme.button?.radius ?? "md"}
370
404
  onClick={handleItemClick}
371
- icon={renderIcon(item.icon) ?? <IconSquareRounded />}
405
+ icon={
406
+ renderIcon(item.icon, ui.sizes.icon.sm) ?? (
407
+ <IconSquareRounded size={ui.sizes.icon.sm} />
408
+ )
409
+ }
372
410
  href={props.item.href as any}
373
411
  target={props.item.target}
374
412
  {...props.item.actionProps}
@@ -401,6 +439,7 @@ export interface SidebarSpacer extends SidebarAbstractItem {
401
439
 
402
440
  export interface SidebarDivider extends SidebarAbstractItem {
403
441
  type: "divider";
442
+ fill?: true;
404
443
  }
405
444
 
406
445
  export interface SidebarSearch extends SidebarAbstractItem {
@@ -415,6 +454,7 @@ export interface SidebarSection extends SidebarAbstractItem {
415
454
  type: "section";
416
455
  label: string;
417
456
  icon?: ReactNode | ComponentType;
457
+ children?: SidebarNode[];
418
458
  }
419
459
 
420
460
  export interface SidebarMenuItem extends SidebarAbstractItem {
@@ -8,7 +8,7 @@ export const ui = {
8
8
  },
9
9
  sizes: {
10
10
  icon: {
11
- xs: 12,
11
+ xs: 14,
12
12
  sm: 16,
13
13
  md: 20,
14
14
  lg: 24,
@@ -2,12 +2,15 @@ import { ui } from "@alepha/ui";
2
2
  import { type ComponentType, isValidElement, type ReactNode } from "react";
3
3
  import { isComponentType } from "./isComponentType.ts";
4
4
 
5
- export const renderIcon = (icon: ReactNode | ComponentType): ReactNode => {
5
+ export const renderIcon = (
6
+ icon: ReactNode | ComponentType,
7
+ size?: number,
8
+ ): ReactNode => {
6
9
  if (!icon) return null;
7
10
  if (isValidElement(icon)) return icon;
8
11
  if (isComponentType(icon)) {
9
12
  const IconComponent = icon;
10
- return <IconComponent size={ui.sizes.icon.md} />;
13
+ return <IconComponent size={size ?? ui.sizes.icon.md} />;
11
14
  }
12
15
  return icon as ReactNode;
13
16
  };
package/src/core/index.ts CHANGED
@@ -52,10 +52,6 @@ export { default as ControlObject } from "./components/form/ControlObject.tsx";
52
52
  export { default as ControlQueryBuilder } from "./components/form/ControlQueryBuilder.tsx";
53
53
  export { default as ControlSelect } from "./components/form/ControlSelect.tsx";
54
54
  export { default as TypeForm } from "./components/form/TypeForm.tsx";
55
- export {
56
- type AdminShellProps,
57
- default as AdminShell,
58
- } from "./components/layout/AdminShell.tsx";
59
55
  export { default as AlephaMantineProvider } from "./components/layout/AlephaMantineProvider.tsx";
60
56
  export type {
61
57
  AppBarBurger,
@@ -69,6 +65,14 @@ export type {
69
65
  AppBarSpacer,
70
66
  } from "./components/layout/AppBar.tsx";
71
67
  export { default as AppBar } from "./components/layout/AppBar.tsx";
68
+ export type { BreadcrumbProps } from "./components/layout/Breadcrumb.tsx";
69
+ export { default as Breadcrumb } from "./components/layout/Breadcrumb.tsx";
70
+ export {
71
+ type DashboardShellProps,
72
+ type DashboardShellProps as AdminShellProps,
73
+ default as DashboardShell,
74
+ default as AdminShell,
75
+ } from "./components/layout/DashboardShell.tsx";
72
76
  export { default as Omnibar } from "./components/layout/Omnibar.tsx";
73
77
  export type {
74
78
  SidebarAbstractItem,
@@ -171,7 +175,7 @@ declare module "alepha/react/router" {
171
175
  * - AlertDialog, ConfirmDialog, PromptDialog
172
176
  * - Form controls: Control, ControlArray, ControlDate, ControlNumber, ControlObject, ControlSelect, ControlQueryBuilder
173
177
  * - TypeForm for automatic form generation from TypeBox schemas
174
- * - AdminShell layout component
178
+ * - DashboardShell layout component
175
179
  * - AppBar with configurable elements
176
180
  * - Sidebar navigation with sections and menu items
177
181
  * - Omnibar for command palette / search
@@ -29,13 +29,13 @@
29
29
  --alepha-text: var(--alepha-text-light);
30
30
  }
31
31
 
32
- #root {
33
- display: flex;
34
- background-color: var(--alepha-background);
35
- color: var(--alepha-text);
36
- min-height: 100dvh;
37
- flex-direction: column;
38
- }
32
+ /*#root {*/
33
+ /* display: flex;*/
34
+ /* background-color: var(--alepha-background);*/
35
+ /* color: var(--alepha-text);*/
36
+ /* min-height: 100dvh;*/
37
+ /* flex-direction: column;*/
38
+ /*}*/
39
39
 
40
40
  /* ------------------------------------------------------------------------------------------------------------------ */
41
41
 
@@ -8,14 +8,38 @@ export const capitalize = (str: string): string => {
8
8
  return str.charAt(0).toUpperCase() + str.slice(1);
9
9
  };
10
10
 
11
+ /**
12
+ * Converts camelCase or snake_case to Title Case with spaces.
13
+ *
14
+ * @example
15
+ * toTitleCase("userName") // "User Name"
16
+ * toTitleCase("first_name") // "First Name"
17
+ * toTitleCase("email") // "Email"
18
+ */
19
+ export const toTitleCase = (str: string): string => {
20
+ return str
21
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase -> camel Case
22
+ .replace(/_/g, " ") // snake_case -> snake case
23
+ .replace(/\b\w/g, (c) => c.toUpperCase()); // capitalize words
24
+ };
25
+
11
26
  /**
12
27
  * Converts a path or identifier string into a pretty display name.
13
- * Removes slashes and capitalizes the first letter.
28
+ * For paths like "/contacts/0/name", extracts just the field name "Name".
29
+ * Handles camelCase and snake_case conversion to Title Case.
14
30
  *
15
31
  * @example
16
- * prettyName("/userName") // "UserName"
17
- * prettyName("email") // "Email"
32
+ * prettyName("/userName") // "User Name"
33
+ * prettyName("/contacts/0/email") // "Email"
34
+ * prettyName("/address/streetName") // "Street Name"
35
+ * prettyName("first_name") // "First Name"
18
36
  */
19
37
  export const prettyName = (name: string): string => {
20
- return capitalize(name.replaceAll("/", ""));
38
+ // Split by slash and filter out empty strings and numeric indices
39
+ const segments = name.split("/").filter((s) => s && !/^\d+$/.test(s));
40
+
41
+ // Use the last non-numeric segment as the field name
42
+ const fieldName = segments[segments.length - 1] || name.replaceAll("/", "");
43
+
44
+ return toTitleCase(fieldName);
21
45
  };
@@ -1,4 +1,8 @@
1
- import { ActionButton, AdminShell, AlephaMantineProvider } from "@alepha/ui";
1
+ import {
2
+ ActionButton,
3
+ AlephaMantineProvider,
4
+ DashboardShell,
5
+ } from "@alepha/ui";
2
6
  import { IconArrowLeft } from "@tabler/icons-react";
3
7
  import { useRouter } from "alepha/react/router";
4
8
  import type { DemoRouter } from "../DemoRouter.ts";
@@ -7,7 +11,7 @@ const DemoLayout = () => {
7
11
  const router = useRouter<DemoRouter>();
8
12
  return (
9
13
  <AlephaMantineProvider>
10
- <AdminShell
14
+ <DashboardShell
11
15
  appShellMainProps={{ h: "100%" }}
12
16
  appBarProps={{
13
17
  items: [
@@ -1,3 +0,0 @@
1
- import { t as AdminApiKeys_default } from "./AdminApiKeys-GMORg-1l.js";
2
-
3
- export { AdminApiKeys_default as default };
@@ -1,3 +0,0 @@
1
- import { t as AdminAudits_default } from "./AdminAudits-pkWrjq1Z.js";
2
-
3
- export { AdminAudits_default as default };
@@ -1,3 +0,0 @@
1
- import { t as AdminFiles_default } from "./AdminFiles-WeQbsCsl.js";
2
-
3
- export { AdminFiles_default as default };
@@ -1,3 +0,0 @@
1
- import { t as AdminJobs_default } from "./AdminJobs-B-q9iGO3.js";
2
-
3
- export { AdminJobs_default as default };
@@ -1 +0,0 @@
1
- {"version":3,"file":"AdminLayout-BqZiXx4H.js","names":[],"sources":["../../src/admin/components/AdminLayout.tsx"],"sourcesContent":["import {\n ActionButton,\n AdminShell,\n type AdminShellProps,\n AlephaMantineProvider,\n OmnibarButton,\n} from \"@alepha/ui\";\nimport { UserButton } from \"@alepha/ui/auth\";\nimport { IconArrowLeft } from \"@tabler/icons-react\";\n\nexport interface AdminLayoutProps {\n adminShellProps?: AdminShellProps;\n}\n\nconst AdminLayout = (props: AdminLayoutProps) => {\n return (\n <AlephaMantineProvider>\n <AdminShell\n appBarProps={{\n items: [\n {\n element: (\n <ActionButton\n variant={\"subtle\"}\n icon={IconArrowLeft}\n href={\"/\"}\n />\n ),\n position: \"left\",\n },\n {\n element: <OmnibarButton />,\n position: \"center\",\n },\n {\n element: <UserButton />,\n position: \"right\",\n },\n {\n type: \"dark\",\n position: \"right\",\n },\n ],\n }}\n sidebarResizable\n sidebarProps={{\n autoPopulateMenu: {\n startsWith: \"/admin\",\n },\n }}\n {...props.adminShellProps}\n />\n </AlephaMantineProvider>\n );\n};\n\nexport default AdminLayout;\n"],"mappings":";;;;;;AAcA,MAAM,eAAe,UAA4B;AAC/C,QACE,oBAAC,mCACC,oBAAC;EACC,aAAa,EACX,OAAO;GACL;IACE,SACE,oBAAC;KACC,SAAS;KACT,MAAM;KACN,MAAM;MACN;IAEJ,UAAU;IACX;GACD;IACE,SAAS,oBAAC,kBAAgB;IAC1B,UAAU;IACX;GACD;IACE,SAAS,oBAAC,eAAa;IACvB,UAAU;IACX;GACD;IACE,MAAM;IACN,UAAU;IACX;GACF,EACF;EACD;EACA,cAAc,EACZ,kBAAkB,EAChB,YAAY,UACb,EACF;EACD,GAAI,MAAM;GACV,GACoB;;AAI5B,0BAAe"}
@@ -1,3 +0,0 @@
1
- import { t as AdminNotifications_default } from "./AdminNotifications-Ds5Un0NJ.js";
2
-
3
- export { AdminNotifications_default as default };
@@ -1,3 +0,0 @@
1
- import { t as AdminParameters_default } from "./AdminParameters-CfDUpc78.js";
2
-
3
- export { AdminParameters_default as default };