@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.
- package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
- package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
- package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
- package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
- package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
- package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
- package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
- package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
- package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-CsjvpeD1.js} +6 -9
- package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
- package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
- package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
- package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
- package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
- package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
- package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
- package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
- package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
- package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
- package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
- package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
- package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
- package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
- package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
- package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
- package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
- package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
- package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
- package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
- package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
- package/dist/admin/index.d.ts +22 -10
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +47 -48
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
- package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
- package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
- package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
- package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
- package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
- package/dist/auth/Login-DS_OqA0G.js.map +1 -0
- package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
- package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
- package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
- package/dist/auth/Register-BRR2_gux.js.map +1 -0
- package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
- package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
- package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
- package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
- package/dist/auth/index.d.ts +14 -14
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +13 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
- package/dist/core/index.d.ts +147 -68
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +349 -287
- package/dist/core/index.js.map +1 -1
- package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
- package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
- package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
- package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
- package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
- package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
- package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
- package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
- package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
- package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
- package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
- package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
- package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
- package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
- package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
- package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
- package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
- package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
- package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
- package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
- package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
- package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
- package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
- package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
- package/dist/demo/index.d.ts +2 -2
- package/dist/demo/index.d.ts.map +1 -1
- package/dist/demo/index.js +15 -15
- package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
- package/package.json +5 -3
- package/src/admin/AdminRouter.ts +15 -24
- package/src/admin/components/AdminLayout.tsx +6 -9
- package/src/admin/components/parameters/AdminParameters.tsx +154 -76
- package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
- package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
- package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
- package/src/admin/components/parameters/ParameterTree.tsx +280 -104
- package/src/admin/components/parameters/types.ts +3 -3
- package/src/admin/primitives/$uiAdmin.ts +2 -2
- package/src/auth/AuthRouter.ts +1 -0
- package/src/core/components/buttons/ActionButton.tsx +4 -15
- package/src/core/components/buttons/DarkModeButton.tsx +8 -4
- package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
- package/src/core/components/form/Control.tsx +10 -32
- package/src/core/components/form/ControlArray.tsx +200 -89
- package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
- package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
- package/src/core/components/layout/Breadcrumb.tsx +91 -0
- package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
- package/src/core/components/layout/Sidebar.tsx +58 -18
- package/src/core/constants/ui.ts +1 -1
- package/src/core/helpers/renderIcon.tsx +5 -2
- package/src/core/index.ts +9 -5
- package/src/core/styles.css +7 -7
- package/src/core/utils/string.ts +28 -4
- package/src/demo/components/DemoLayout.tsx +6 -2
- package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
- package/dist/admin/AdminAudits-8SM96viT.js +0 -3
- package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
- package/dist/admin/AdminJobs-CED1syCn.js +0 -3
- package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
- package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
- package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
- package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
- package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
- package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
- package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
- package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
- package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
- package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
- package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
- package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
- package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
- package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
- package/dist/auth/Login-BBqTosqZ.js.map +0 -1
- package/dist/auth/Login-CoU63mMR.js +0 -4
- package/dist/auth/Register-BV_oa_AK.js +0 -4
- package/dist/auth/Register-Ce675Crg.js.map +0 -1
- package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
- package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
- package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
- package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
- package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
- package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
- package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
- package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
- package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
- package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
- package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
- package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
- package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
- package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
- 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
|
|
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
|
|
90
|
+
const DashboardShell = (props: DashboardShellProps) => {
|
|
63
91
|
const router = useRouter();
|
|
64
92
|
const [sidebar, setSidebar] = useStore(alephaSidebarAtom);
|
|
65
|
-
const { opened
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
265
|
+
const hasAppBar = props.appBarProps || props.header;
|
|
239
266
|
|
|
240
|
-
const
|
|
241
|
-
const
|
|
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
|
-
|
|
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:
|
|
315
|
+
footer={props.footer ? { height: fHeight } : undefined}
|
|
287
316
|
{...props.appShellProps}
|
|
288
317
|
>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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
|
|
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(--
|
|
65
|
+
bg={"var(--mantine-color-default-border)"}
|
|
58
66
|
my={"xs"}
|
|
59
|
-
mx={
|
|
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
|
|
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
|
-
<
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
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 <
|
|
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 ??
|
|
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(--
|
|
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={
|
|
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 {
|
package/src/core/constants/ui.ts
CHANGED
|
@@ -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 = (
|
|
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
|
-
* -
|
|
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
|
package/src/core/styles.css
CHANGED
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
--alepha-text: var(--alepha-text-light);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
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
|
|
package/src/core/utils/string.ts
CHANGED
|
@@ -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
|
-
*
|
|
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") // "
|
|
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
|
-
|
|
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 {
|
|
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
|
-
<
|
|
14
|
+
<DashboardShell
|
|
11
15
|
appShellMainProps={{ h: "100%" }}
|
|
12
16
|
appBarProps={{
|
|
13
17
|
items: [
|
|
@@ -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"}
|