@alepha/ui 0.14.1 → 0.14.3
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/AdminAudits-B3EhKhN7.js +3 -0
- package/dist/admin/{AdminAudits-CwvH8e8c.js → AdminAudits-DIrCCPk3.js} +3 -2
- package/dist/admin/AdminAudits-DIrCCPk3.js.map +1 -0
- package/dist/admin/AdminFiles-C8OG4dtD.js +3 -0
- package/dist/admin/{AdminFiles-C_w1tb_x.js → AdminFiles-RsL178Ta.js} +2 -2
- package/dist/admin/{AdminFiles-C_w1tb_x.js.map → AdminFiles-RsL178Ta.js.map} +1 -1
- package/dist/admin/AdminNotifications-BSL4B2fQ.js +3 -0
- package/dist/admin/{AdminNotifications-DuYy74AN.js → AdminNotifications-cIbywWKi.js} +2 -2
- package/dist/admin/AdminNotifications-cIbywWKi.js.map +1 -0
- package/dist/admin/{AdminParameters-DYg48Jwe.js → AdminParameters-BKObzzpN.js} +1 -1
- package/dist/admin/{AdminParameters-YagqWTG3.js → AdminParameters-D-q3Qmhv.js} +2 -2
- package/dist/admin/{AdminParameters-YagqWTG3.js.map → AdminParameters-D-q3Qmhv.js.map} +1 -1
- package/dist/admin/AdminSessions-DHG9zPfr.js +3 -0
- package/dist/admin/{AdminSessions-BCjgJ-93.js → AdminSessions-vOgkrQ2U.js} +3 -2
- package/dist/admin/AdminSessions-vOgkrQ2U.js.map +1 -0
- package/dist/admin/{AdminUserAudits-B_PUXCKC.js → AdminUserAudits-CSsN1fIC.js} +3 -2
- package/dist/admin/AdminUserAudits-CSsN1fIC.js.map +1 -0
- package/dist/admin/{AdminUserAudits-D7cTcElL.js → AdminUserAudits-DmAnivo3.js} +1 -1
- package/dist/admin/{AdminUserCreate-DzfRbGZ4.js → AdminUserCreate-B72nu-3W.js} +3 -2
- package/dist/admin/AdminUserCreate-B72nu-3W.js.map +1 -0
- package/dist/admin/{AdminUserCreate-oUA1KDIl.js → AdminUserCreate-DpA13zwj.js} +1 -1
- package/dist/admin/AdminUserDetails-CKM2IEMr.js +475 -0
- package/dist/admin/AdminUserDetails-CKM2IEMr.js.map +1 -0
- package/dist/admin/{AdminUserDetails-y1H5DW8Y.js → AdminUserDetails-Zib_B6Al.js} +1 -1
- package/dist/admin/{AdminUserLayout-Dejnz13m.js → AdminUserLayout-BNBOEiAO.js} +1 -1
- package/dist/admin/AdminUserLayout-D7En9UBq.js +334 -0
- package/dist/admin/AdminUserLayout-D7En9UBq.js.map +1 -0
- package/dist/admin/AdminUserSessions-D9X2_HMA.js +3 -0
- package/dist/admin/{AdminUserSessions-DO9H85O-.js → AdminUserSessions-DEaGu6n6.js} +3 -2
- package/dist/admin/AdminUserSessions-DEaGu6n6.js.map +1 -0
- package/dist/admin/{AdminUserSettings-B3jA8g3p.js → AdminUserSettings-Di73D7g2.js} +8 -6
- package/dist/admin/AdminUserSettings-Di73D7g2.js.map +1 -0
- package/dist/admin/AdminUserSettings-yI-JECf5.js +3 -0
- package/dist/admin/{AdminUsers-ebbrJBT0.js → AdminUsers-BnGIRvmV.js} +3 -2
- package/dist/admin/AdminUsers-BnGIRvmV.js.map +1 -0
- package/dist/admin/AdminUsers-CG9-2Z8W.js +3 -0
- package/dist/admin/index.d.ts +25 -25
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +37 -36
- package/dist/admin/index.js.map +1 -1
- package/dist/auth/{AuthLayout-BAZJHzDG.js → AuthLayout-B1sUB8fB.js} +2 -2
- package/dist/auth/AuthLayout-B1sUB8fB.js.map +1 -0
- package/dist/auth/Login-BWi-pPbO.js +4 -0
- package/dist/auth/{Login-CeNZZjrr.js → Login-Cjxv3EDi.js} +2 -2
- package/dist/auth/Login-Cjxv3EDi.js.map +1 -0
- package/dist/auth/{Register-s4ENeyiE.js → Register-BKBIpHhW.js} +3 -2
- package/dist/auth/Register-BKBIpHhW.js.map +1 -0
- package/dist/auth/Register-CtdvihIM.js +4 -0
- package/dist/auth/ResetPassword-BUdM7T_R.js +3 -0
- package/dist/auth/{ResetPassword-GLIFkJT7.js → ResetPassword-DvqD_1SJ.js} +3 -2
- package/dist/auth/ResetPassword-DvqD_1SJ.js.map +1 -0
- package/dist/auth/VerifyEmail-BYmtnkEl.js +3 -0
- package/dist/auth/{VerifyEmail-R79sSej_.js → VerifyEmail-VaBruOnO.js} +3 -2
- package/dist/auth/VerifyEmail-VaBruOnO.js.map +1 -0
- package/dist/auth/index.d.ts +11 -11
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +10 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +36 -55
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +50 -350
- package/dist/core/index.js.map +1 -1
- package/dist/demo/DemoDataTable-2mzzf__a.js +150 -0
- package/dist/demo/DemoDataTable-2mzzf__a.js.map +1 -0
- package/dist/demo/DemoHome-CnuL5WV9.js +25 -0
- package/dist/demo/DemoHome-CnuL5WV9.js.map +1 -0
- package/dist/demo/DemoHome-D6Z7EE4V.js +3 -0
- package/dist/demo/DemoJsonViewer-CYUggLop.js +4 -0
- package/dist/demo/DemoJsonViewer-NUGst5wW.js +430 -0
- package/dist/demo/DemoJsonViewer-NUGst5wW.js.map +1 -0
- package/dist/demo/DemoLayout-ZFDzyvY3.js +3 -0
- package/dist/demo/DemoLayout-dvbeuBBf.js +47 -0
- package/dist/demo/DemoLayout-dvbeuBBf.js.map +1 -0
- package/dist/demo/DemoLogin--wE44i23.js +327 -0
- package/dist/demo/DemoLogin--wE44i23.js.map +1 -0
- package/dist/demo/DemoRegister-BtrMksx6.js +488 -0
- package/dist/demo/DemoRegister-BtrMksx6.js.map +1 -0
- package/dist/demo/DemoResetPassword-DVXiiiX7.js +341 -0
- package/dist/demo/DemoResetPassword-DVXiiiX7.js.map +1 -0
- package/dist/demo/DemoSidebar-DWnjYHoP.js +82 -0
- package/dist/demo/DemoSidebar-DWnjYHoP.js.map +1 -0
- package/dist/demo/DemoTypeForm-P5_VInW2.js +83 -0
- package/dist/demo/DemoTypeForm-P5_VInW2.js.map +1 -0
- package/dist/demo/DemoVerifyEmail-C_ooC5u8.js +152 -0
- package/dist/demo/DemoVerifyEmail-C_ooC5u8.js.map +1 -0
- package/dist/demo/IconGoogle-DvmFiEDB.js +58 -0
- package/dist/demo/IconGoogle-DvmFiEDB.js.map +1 -0
- package/dist/demo/Showcase-vemLuO2t.js +187 -0
- package/dist/demo/Showcase-vemLuO2t.js.map +1 -0
- package/dist/demo/index.d.ts +97 -0
- package/dist/demo/index.d.ts.map +1 -0
- package/dist/demo/index.js +121 -0
- package/dist/demo/index.js.map +1 -0
- package/dist/json/index.d.ts +58 -0
- package/dist/json/index.d.ts.map +1 -0
- package/dist/json/index.js +325 -0
- package/dist/json/index.js.map +1 -0
- package/package.json +25 -14
- package/src/admin/AdminRouter.ts +23 -20
- package/src/admin/MainRouter.ts +2 -2
- package/src/admin/components/audits/AdminAudits.tsx +4 -3
- package/src/admin/components/jobs/AdminJobs.tsx +2 -2
- package/src/admin/components/notifications/AdminNotifications.tsx +2 -2
- package/src/admin/components/parameters/AdminParameters.tsx +2 -2
- package/src/admin/components/sessions/AdminSessions.tsx +4 -3
- package/src/admin/components/shared/AdminResourceHeader.tsx +281 -0
- package/src/admin/components/shared/AdminResourceTabs.tsx +94 -0
- package/src/admin/components/shared/index.ts +10 -0
- package/src/admin/components/users/AdminUserAudits.tsx +4 -3
- package/src/admin/components/users/AdminUserCreate.tsx +4 -3
- package/src/admin/components/users/AdminUserDetails.tsx +339 -86
- package/src/admin/components/users/AdminUserLayout.tsx +165 -113
- package/src/admin/components/users/AdminUserSessions.tsx +4 -3
- package/src/admin/components/users/AdminUserSettings.tsx +12 -6
- package/src/admin/components/users/AdminUsers.tsx +8 -3
- package/src/auth/AuthRouter.ts +1 -1
- package/src/auth/components/AuthLayout.tsx +1 -1
- package/src/auth/components/Login.tsx +1 -1
- package/src/auth/components/Register.tsx +2 -1
- package/src/auth/components/ResetPassword.tsx +2 -1
- package/src/auth/components/VerifyEmail.tsx +2 -1
- package/src/auth/components/buttons/UserButton.tsx +1 -1
- package/src/core/RootRouter.ts +1 -1
- package/src/core/components/buttons/ActionButton.tsx +3 -4
- package/src/core/components/form/Control.tsx +12 -1
- package/src/core/components/form/ControlNumber.tsx +5 -0
- package/src/core/components/form/TypeForm.tsx +3 -2
- package/src/core/components/layout/AdminShell.tsx +2 -1
- package/src/core/components/layout/AlephaMantineProvider.tsx +7 -2
- package/src/core/components/layout/Omnibar.tsx +2 -1
- package/src/core/components/layout/Sidebar.tsx +18 -18
- package/src/core/index.ts +1 -2
- package/src/core/services/DialogService.tsx +0 -17
- package/{styles.css → src/core/styles.css} +1 -5
- package/src/demo/DemoRouter.ts +123 -0
- package/src/demo/components/DemoHome.tsx +29 -0
- package/src/demo/components/DemoLayout.tsx +52 -0
- package/src/demo/components/auth/DemoLogin.tsx +130 -0
- package/src/demo/components/auth/DemoRegister.tsx +144 -0
- package/src/demo/components/auth/DemoResetPassword.tsx +69 -0
- package/src/demo/components/auth/DemoVerifyEmail.tsx +28 -0
- package/src/demo/components/core/DemoDataTable.tsx +174 -0
- package/src/demo/components/core/DemoSidebar.tsx +85 -0
- package/src/demo/components/core/DemoTypeForm.tsx +69 -0
- package/src/demo/components/json/DemoJsonViewer.tsx +128 -0
- package/src/demo/components/shared/MacWindow.tsx +105 -0
- package/src/demo/components/shared/Showcase.tsx +112 -0
- package/src/demo/index.ts +30 -0
- package/src/demo/styles.css +0 -0
- package/src/json/components/JsonViewer.css +25 -0
- package/src/json/components/JsonViewer.tsx +526 -0
- package/src/json/extensions/DialogService.tsx +31 -0
- package/src/json/index.ts +5 -0
- package/src/json/styles.css +1 -0
- package/dist/admin/AdminAudits-CwvH8e8c.js.map +0 -1
- package/dist/admin/AdminAudits-Dv8Vk_6r.js +0 -3
- package/dist/admin/AdminFiles-5CPA3lQk.js +0 -3
- package/dist/admin/AdminNotifications-DLjmZWtf.js +0 -3
- package/dist/admin/AdminNotifications-DuYy74AN.js.map +0 -1
- package/dist/admin/AdminSessions-BCjgJ-93.js.map +0 -1
- package/dist/admin/AdminSessions-DEh2uN-4.js +0 -3
- package/dist/admin/AdminUserAudits-B_PUXCKC.js.map +0 -1
- package/dist/admin/AdminUserCreate-DzfRbGZ4.js.map +0 -1
- package/dist/admin/AdminUserDetails-DeTrJm-t.js +0 -221
- package/dist/admin/AdminUserDetails-DeTrJm-t.js.map +0 -1
- package/dist/admin/AdminUserLayout-CsfrrZkD.js +0 -150
- package/dist/admin/AdminUserLayout-CsfrrZkD.js.map +0 -1
- package/dist/admin/AdminUserSessions-Bbhcpz4k.js +0 -3
- package/dist/admin/AdminUserSessions-DO9H85O-.js.map +0 -1
- package/dist/admin/AdminUserSettings-B3jA8g3p.js.map +0 -1
- package/dist/admin/AdminUserSettings-CE0xpbQc.js +0 -3
- package/dist/admin/AdminUsers-CegGZDhW.js +0 -3
- package/dist/admin/AdminUsers-ebbrJBT0.js.map +0 -1
- package/dist/auth/AuthLayout-BAZJHzDG.js.map +0 -1
- package/dist/auth/Login-CeNZZjrr.js.map +0 -1
- package/dist/auth/Login-hQcu1nlu.js +0 -4
- package/dist/auth/Register-B6HBNVHS.js +0 -4
- package/dist/auth/Register-s4ENeyiE.js.map +0 -1
- package/dist/auth/ResetPassword-Cjd-W-Nu.js +0 -3
- package/dist/auth/ResetPassword-GLIFkJT7.js.map +0 -1
- package/dist/auth/VerifyEmail-Dc9ABKUw.js +0 -3
- package/dist/auth/VerifyEmail-R79sSej_.js.map +0 -1
- package/src/core/components/data/JsonViewer.tsx +0 -361
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useForm } from "@alepha/react/form";
|
|
2
|
+
import { TypeForm, ui } from "@alepha/ui";
|
|
3
|
+
import { Box, Card, Flex, Text } from "@mantine/core";
|
|
4
|
+
import type { Static, TObject } from "alepha";
|
|
5
|
+
import { type ReactNode, useState } from "react";
|
|
6
|
+
import MacWindow, { type MacWindowProps } from "./MacWindow.tsx";
|
|
7
|
+
|
|
8
|
+
export interface ShowcaseProps<T extends TObject> {
|
|
9
|
+
/**
|
|
10
|
+
* Component title
|
|
11
|
+
*/
|
|
12
|
+
title: string;
|
|
13
|
+
/**
|
|
14
|
+
* Schema for the props configuration
|
|
15
|
+
*/
|
|
16
|
+
schema: T;
|
|
17
|
+
/**
|
|
18
|
+
* Initial values for the props
|
|
19
|
+
*/
|
|
20
|
+
initialValues?: Partial<Static<T>>;
|
|
21
|
+
/**
|
|
22
|
+
* Number of columns for the props form
|
|
23
|
+
*/
|
|
24
|
+
columns?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Render function that receives the current props values
|
|
27
|
+
*/
|
|
28
|
+
children: (props: Static<T>) => ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Additional props for the MacWindow container
|
|
31
|
+
*/
|
|
32
|
+
windowProps?: Partial<MacWindowProps>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Showcase component for demonstrating UI components with interactive props configuration.
|
|
37
|
+
* Uses TypeForm to render a form based on the props schema and displays the component preview.
|
|
38
|
+
*/
|
|
39
|
+
const Showcase = <T extends TObject>({
|
|
40
|
+
title,
|
|
41
|
+
schema,
|
|
42
|
+
initialValues,
|
|
43
|
+
columns = 3,
|
|
44
|
+
children,
|
|
45
|
+
windowProps,
|
|
46
|
+
}: ShowcaseProps<T>) => {
|
|
47
|
+
const [values, setValues] = useState<Record<string, any>>(
|
|
48
|
+
initialValues ?? {},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const form = useForm(
|
|
52
|
+
{
|
|
53
|
+
schema,
|
|
54
|
+
initialValues,
|
|
55
|
+
handler: (values) => {
|
|
56
|
+
setValues(values as Record<string, any>);
|
|
57
|
+
},
|
|
58
|
+
onChange: (key, value) => {
|
|
59
|
+
console.log("onChange", key, value);
|
|
60
|
+
form.submit();
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
[schema],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Flex flex={1} h={"100%"}>
|
|
68
|
+
<Flex
|
|
69
|
+
flex={1}
|
|
70
|
+
bg={ui.colors.background}
|
|
71
|
+
h={"100%"}
|
|
72
|
+
p="xl"
|
|
73
|
+
justify="center"
|
|
74
|
+
align="flex-start"
|
|
75
|
+
style={{ flex: 1, minWidth: 0, overflow: "auto" }}
|
|
76
|
+
>
|
|
77
|
+
<MacWindow title={title} {...windowProps}>
|
|
78
|
+
{children(values as Static<T>)}
|
|
79
|
+
</MacWindow>
|
|
80
|
+
</Flex>
|
|
81
|
+
|
|
82
|
+
<Box
|
|
83
|
+
bg={ui.colors.surface}
|
|
84
|
+
h={"100%"}
|
|
85
|
+
p={"md"}
|
|
86
|
+
style={{
|
|
87
|
+
flex: "0 0 300px",
|
|
88
|
+
overflow: "auto",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Card withBorder shadow="sm" radius="md" bg={ui.colors.elevated}>
|
|
92
|
+
<Card.Section withBorder py={"xs"} inheritPadding>
|
|
93
|
+
<Text size={"xs"} fw={500}>
|
|
94
|
+
{title} Props
|
|
95
|
+
</Text>
|
|
96
|
+
</Card.Section>
|
|
97
|
+
|
|
98
|
+
<Card.Section p={"sm"}>
|
|
99
|
+
<TypeForm
|
|
100
|
+
form={form}
|
|
101
|
+
columns={columns}
|
|
102
|
+
skipSubmitButton
|
|
103
|
+
skipFormElement
|
|
104
|
+
/>
|
|
105
|
+
</Card.Section>
|
|
106
|
+
</Card>
|
|
107
|
+
</Box>
|
|
108
|
+
</Flex>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default Showcase;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AlephaUI } from "@alepha/ui";
|
|
2
|
+
import { $module } from "alepha";
|
|
3
|
+
import { DemoRouter } from "./DemoRouter.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export { default as DemoHome } from "./components/DemoHome.tsx";
|
|
8
|
+
export { default as DemoLayout } from "./components/DemoLayout.tsx";
|
|
9
|
+
export { default as DemoJsonViewer } from "./components/json/DemoJsonViewer.tsx";
|
|
10
|
+
export {
|
|
11
|
+
default as MacWindow,
|
|
12
|
+
type MacWindowProps,
|
|
13
|
+
} from "./components/shared/MacWindow.tsx";
|
|
14
|
+
export {
|
|
15
|
+
default as Showcase,
|
|
16
|
+
type ShowcaseProps,
|
|
17
|
+
} from "./components/shared/Showcase.tsx";
|
|
18
|
+
export { DemoRouter } from "./DemoRouter.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Demo UI Module - Component showcase and documentation
|
|
24
|
+
*
|
|
25
|
+
* @module alepha.ui.demo
|
|
26
|
+
*/
|
|
27
|
+
export const AlephaUIDemo = $module({
|
|
28
|
+
name: "alepha.ui.demo",
|
|
29
|
+
services: [AlephaUI, DemoRouter],
|
|
30
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.alepha-json-viewer-row {
|
|
2
|
+
cursor: pointer;
|
|
3
|
+
user-select: text;
|
|
4
|
+
min-width: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.alepha-json-viewer-value {
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
text-overflow: ellipsis;
|
|
10
|
+
white-space: nowrap;
|
|
11
|
+
min-width: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.alepha-json-viewer-row:hover {
|
|
15
|
+
background-color: var(--mantine-color-default-hover);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.alepha-json-viewer-copy {
|
|
19
|
+
opacity: 0;
|
|
20
|
+
transition: opacity 100ms;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.alepha-json-viewer-row:hover .alepha-json-viewer-copy {
|
|
24
|
+
opacity: 1;
|
|
25
|
+
}
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Group,
|
|
4
|
+
getTreeExpandedState,
|
|
5
|
+
type MantineSize,
|
|
6
|
+
Text,
|
|
7
|
+
Tree,
|
|
8
|
+
useTree,
|
|
9
|
+
} from "@mantine/core";
|
|
10
|
+
import {
|
|
11
|
+
IconCheck,
|
|
12
|
+
IconChevronDown,
|
|
13
|
+
IconChevronRight,
|
|
14
|
+
IconCopy,
|
|
15
|
+
} from "@tabler/icons-react";
|
|
16
|
+
import {
|
|
17
|
+
type CSSProperties,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
useCallback,
|
|
20
|
+
useMemo,
|
|
21
|
+
useState,
|
|
22
|
+
} from "react";
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// TYPES
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
interface JsonViewerProps {
|
|
29
|
+
data: any;
|
|
30
|
+
/**
|
|
31
|
+
* Depth level to expand by default (0 = collapsed, Infinity = all expanded)
|
|
32
|
+
*/
|
|
33
|
+
defaultExpandedDepth?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Maximum nesting depth to render
|
|
36
|
+
*/
|
|
37
|
+
maxDepth?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Size variant
|
|
40
|
+
*/
|
|
41
|
+
size?: MantineSize;
|
|
42
|
+
/**
|
|
43
|
+
* Whether to show quotes around keys and strings
|
|
44
|
+
*/
|
|
45
|
+
showQuotes?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Show copy button on row hover
|
|
48
|
+
*/
|
|
49
|
+
showCopyButton?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Custom value formatter. Return formatted string or undefined to use default.
|
|
52
|
+
*/
|
|
53
|
+
formatValue?: (
|
|
54
|
+
key: string | undefined,
|
|
55
|
+
value: any,
|
|
56
|
+
path: string[],
|
|
57
|
+
) => string | number | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface JsonTreeNode {
|
|
61
|
+
value: string;
|
|
62
|
+
label: string;
|
|
63
|
+
children?: JsonTreeNode[];
|
|
64
|
+
// Custom properties
|
|
65
|
+
nodeValue: any;
|
|
66
|
+
nodeKey: string | undefined;
|
|
67
|
+
path: string[];
|
|
68
|
+
isArrayItem: boolean;
|
|
69
|
+
isRoot?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// CONSTANTS
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
const SIZE_CONFIG: Record<MantineSize, { icon: number; levelOffset: number }> =
|
|
77
|
+
{
|
|
78
|
+
xs: { icon: 14, levelOffset: 16 },
|
|
79
|
+
sm: { icon: 16, levelOffset: 20 },
|
|
80
|
+
md: { icon: 18, levelOffset: 24 },
|
|
81
|
+
lg: { icon: 20, levelOffset: 28 },
|
|
82
|
+
xl: { icon: 22, levelOffset: 32 },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const STYLES = {
|
|
86
|
+
root: {
|
|
87
|
+
fontFamily: "var(--mantine-font-family-monospace)",
|
|
88
|
+
} satisfies CSSProperties,
|
|
89
|
+
chevron: {
|
|
90
|
+
flexShrink: 0,
|
|
91
|
+
color: "var(--mantine-color-dimmed)",
|
|
92
|
+
} satisfies CSSProperties,
|
|
93
|
+
key: {
|
|
94
|
+
color: "var(--mantine-color-cyan-text)",
|
|
95
|
+
fontWeight: 500,
|
|
96
|
+
} satisfies CSSProperties,
|
|
97
|
+
colon: {
|
|
98
|
+
color: "var(--mantine-color-dimmed)",
|
|
99
|
+
} satisfies CSSProperties,
|
|
100
|
+
string: {
|
|
101
|
+
color: "var(--mantine-color-teal-text)",
|
|
102
|
+
} satisfies CSSProperties,
|
|
103
|
+
number: {
|
|
104
|
+
color: "var(--mantine-color-blue-text)",
|
|
105
|
+
} satisfies CSSProperties,
|
|
106
|
+
boolean: {
|
|
107
|
+
color: "var(--mantine-color-violet-text)",
|
|
108
|
+
} satisfies CSSProperties,
|
|
109
|
+
null: {
|
|
110
|
+
color: "var(--mantine-color-dimmed)",
|
|
111
|
+
fontStyle: "italic",
|
|
112
|
+
} satisfies CSSProperties,
|
|
113
|
+
preview: {
|
|
114
|
+
color: "var(--mantine-color-dimmed)",
|
|
115
|
+
} satisfies CSSProperties,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// HELPERS
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
const getValueType = (val: any): string => {
|
|
123
|
+
if (val === null) return "null";
|
|
124
|
+
if (val === undefined) return "undefined";
|
|
125
|
+
if (Array.isArray(val)) return "array";
|
|
126
|
+
return typeof val;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Convert JSON to tree data structure
|
|
130
|
+
function buildTreeNodes(
|
|
131
|
+
data: any,
|
|
132
|
+
path: string[] = [],
|
|
133
|
+
key?: string,
|
|
134
|
+
isArrayItem = false,
|
|
135
|
+
maxDepth = 10,
|
|
136
|
+
): JsonTreeNode | null {
|
|
137
|
+
const currentPath = key !== undefined ? [...path, key] : path;
|
|
138
|
+
const nodeId = currentPath.length > 0 ? currentPath.join(".") : "root";
|
|
139
|
+
|
|
140
|
+
if (currentPath.length > maxDepth) {
|
|
141
|
+
return {
|
|
142
|
+
value: nodeId,
|
|
143
|
+
label: key ?? "",
|
|
144
|
+
nodeValue: data,
|
|
145
|
+
nodeKey: key,
|
|
146
|
+
path: currentPath,
|
|
147
|
+
isArrayItem,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const type = getValueType(data);
|
|
152
|
+
|
|
153
|
+
if (type === "object" || type === "array") {
|
|
154
|
+
const entries =
|
|
155
|
+
type === "array"
|
|
156
|
+
? (data as any[]).map((v, i) => [String(i), v] as const)
|
|
157
|
+
: Object.entries(data);
|
|
158
|
+
|
|
159
|
+
const children = entries
|
|
160
|
+
.map(([k, v]) =>
|
|
161
|
+
buildTreeNodes(v, currentPath, k, type === "array", maxDepth),
|
|
162
|
+
)
|
|
163
|
+
.filter((n): n is JsonTreeNode => n !== null);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
value: nodeId,
|
|
167
|
+
label: key ?? "",
|
|
168
|
+
nodeValue: data,
|
|
169
|
+
nodeKey: key,
|
|
170
|
+
path: currentPath,
|
|
171
|
+
isArrayItem,
|
|
172
|
+
children: children.length > 0 ? children : undefined,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
value: nodeId,
|
|
178
|
+
label: key ?? "",
|
|
179
|
+
nodeValue: data,
|
|
180
|
+
nodeKey: key,
|
|
181
|
+
path: currentPath,
|
|
182
|
+
isArrayItem,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get all expandable node IDs up to a certain depth
|
|
187
|
+
function getExpandedIds(
|
|
188
|
+
nodes: JsonTreeNode[],
|
|
189
|
+
targetDepth: number,
|
|
190
|
+
currentDepth = 0,
|
|
191
|
+
): string[] {
|
|
192
|
+
if (currentDepth >= targetDepth) return [];
|
|
193
|
+
const ids: string[] = [];
|
|
194
|
+
for (const node of nodes) {
|
|
195
|
+
if (node.children) {
|
|
196
|
+
ids.push(node.value);
|
|
197
|
+
ids.push(...getExpandedIds(node.children, targetDepth, currentDepth + 1));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return ids;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// COPY BUTTON COMPONENT
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
const CopyButton = ({
|
|
208
|
+
value,
|
|
209
|
+
iconSize,
|
|
210
|
+
}: {
|
|
211
|
+
value: string;
|
|
212
|
+
iconSize: number;
|
|
213
|
+
}) => {
|
|
214
|
+
const [copied, setCopied] = useState(false);
|
|
215
|
+
|
|
216
|
+
const handleCopy = useCallback(
|
|
217
|
+
(e: React.MouseEvent) => {
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
navigator.clipboard.writeText(value);
|
|
220
|
+
setCopied(true);
|
|
221
|
+
setTimeout(() => setCopied(false), 1500);
|
|
222
|
+
},
|
|
223
|
+
[value],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<ActionIcon
|
|
228
|
+
size={iconSize + 4}
|
|
229
|
+
variant="transparent"
|
|
230
|
+
c={copied ? "green" : "dimmed"}
|
|
231
|
+
onClick={handleCopy}
|
|
232
|
+
className="alepha-json-viewer-copy"
|
|
233
|
+
>
|
|
234
|
+
{copied ? <IconCheck size={iconSize} /> : <IconCopy size={iconSize} />}
|
|
235
|
+
</ActionIcon>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// =============================================================================
|
|
240
|
+
// ROW NODE COMPONENT
|
|
241
|
+
// =============================================================================
|
|
242
|
+
|
|
243
|
+
interface RowNodeProps {
|
|
244
|
+
node: JsonTreeNode;
|
|
245
|
+
expanded: boolean;
|
|
246
|
+
hasChildren: boolean;
|
|
247
|
+
elementProps: any;
|
|
248
|
+
size: MantineSize;
|
|
249
|
+
config: { icon: number; levelOffset: number };
|
|
250
|
+
showQuotes: boolean;
|
|
251
|
+
showCopyButton: boolean;
|
|
252
|
+
renderValue: (val: any, key: string | undefined, path: string[]) => ReactNode;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const RowNode = ({
|
|
256
|
+
node,
|
|
257
|
+
expanded,
|
|
258
|
+
hasChildren,
|
|
259
|
+
elementProps,
|
|
260
|
+
size,
|
|
261
|
+
config,
|
|
262
|
+
showQuotes,
|
|
263
|
+
showCopyButton,
|
|
264
|
+
renderValue,
|
|
265
|
+
}: RowNodeProps) => {
|
|
266
|
+
const { nodeValue, nodeKey, path, isArrayItem, isRoot } = node;
|
|
267
|
+
const type = getValueType(nodeValue);
|
|
268
|
+
const isExpandable = type === "object" || type === "array";
|
|
269
|
+
|
|
270
|
+
const getPreview = () => {
|
|
271
|
+
if (!isExpandable) return null;
|
|
272
|
+
const entries = type === "array" ? nodeValue : Object.keys(nodeValue);
|
|
273
|
+
const count = entries.length;
|
|
274
|
+
const label = type === "array" ? "item" : "key";
|
|
275
|
+
|
|
276
|
+
// For root node or collapsed nodes, show the count
|
|
277
|
+
if (!expanded) {
|
|
278
|
+
return (
|
|
279
|
+
<Text fs={"italic"} component="span" size={size} style={STYLES.preview}>
|
|
280
|
+
{count === 0
|
|
281
|
+
? type === "array"
|
|
282
|
+
? "[]"
|
|
283
|
+
: "{}"
|
|
284
|
+
: type === "array"
|
|
285
|
+
? `[ ${count} ${count === 1 ? label : `${label}s`} ]`
|
|
286
|
+
: `{ ${count} ${count === 1 ? label : `${label}s`} }`}
|
|
287
|
+
</Text>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const getCopyValue = () =>
|
|
295
|
+
isExpandable ? JSON.stringify(nodeValue, null, 2) : String(nodeValue ?? "");
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<Group
|
|
299
|
+
gap={6}
|
|
300
|
+
wrap="nowrap"
|
|
301
|
+
{...elementProps}
|
|
302
|
+
className={`alepha-json-viewer-row ${elementProps.className || ""}`}
|
|
303
|
+
>
|
|
304
|
+
{hasChildren ? (
|
|
305
|
+
expanded ? (
|
|
306
|
+
<IconChevronDown size={config.icon} style={STYLES.chevron} />
|
|
307
|
+
) : (
|
|
308
|
+
<IconChevronRight size={config.icon} style={STYLES.chevron} />
|
|
309
|
+
)
|
|
310
|
+
) : (
|
|
311
|
+
<span style={{ width: config.icon, flexShrink: 0 }} />
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{nodeKey !== undefined && !isArrayItem && (
|
|
315
|
+
<Text component="span" size={size}>
|
|
316
|
+
<span style={STYLES.key}>
|
|
317
|
+
{showQuotes ? `"${nodeKey}"` : nodeKey}
|
|
318
|
+
</span>
|
|
319
|
+
<span style={STYLES.colon}>:</span>
|
|
320
|
+
</Text>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{nodeKey !== undefined && isArrayItem && (
|
|
324
|
+
<Text component="span" size={size}>
|
|
325
|
+
<span style={STYLES.key}>{nodeKey}</span>
|
|
326
|
+
<span style={STYLES.colon}>:</span>
|
|
327
|
+
</Text>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{hasChildren ? (
|
|
331
|
+
getPreview()
|
|
332
|
+
) : isExpandable ? (
|
|
333
|
+
type === "array" ? (
|
|
334
|
+
<Text component="span" size={size} style={STYLES.preview}>
|
|
335
|
+
[]
|
|
336
|
+
</Text>
|
|
337
|
+
) : (
|
|
338
|
+
<Text component="span" size={size} style={STYLES.preview}>
|
|
339
|
+
{"{}"}
|
|
340
|
+
</Text>
|
|
341
|
+
)
|
|
342
|
+
) : (
|
|
343
|
+
renderValue(nodeValue, nodeKey, path)
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{showCopyButton && (
|
|
347
|
+
<CopyButton value={getCopyValue()} iconSize={config.icon} />
|
|
348
|
+
)}
|
|
349
|
+
</Group>
|
|
350
|
+
);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// MAIN COMPONENT
|
|
355
|
+
// =============================================================================
|
|
356
|
+
|
|
357
|
+
export const JsonViewer = ({
|
|
358
|
+
data,
|
|
359
|
+
defaultExpandedDepth = 2,
|
|
360
|
+
maxDepth = 10,
|
|
361
|
+
size = "sm",
|
|
362
|
+
showQuotes = false,
|
|
363
|
+
showCopyButton = true,
|
|
364
|
+
formatValue,
|
|
365
|
+
}: JsonViewerProps) => {
|
|
366
|
+
const config = SIZE_CONFIG[size] || SIZE_CONFIG.sm;
|
|
367
|
+
|
|
368
|
+
// Build tree data from JSON with root wrapper
|
|
369
|
+
const treeData = useMemo(() => {
|
|
370
|
+
const type = getValueType(data);
|
|
371
|
+
|
|
372
|
+
// For objects and arrays, create a root node wrapper
|
|
373
|
+
if (type === "object" || type === "array") {
|
|
374
|
+
const entries =
|
|
375
|
+
type === "array"
|
|
376
|
+
? (data as any[]).map((v, i) => [String(i), v] as const)
|
|
377
|
+
: Object.entries(data);
|
|
378
|
+
const children = entries
|
|
379
|
+
.map(([k, v]) => buildTreeNodes(v, [], k, type === "array", maxDepth))
|
|
380
|
+
.filter((n): n is JsonTreeNode => n !== null);
|
|
381
|
+
|
|
382
|
+
const rootNode: JsonTreeNode = {
|
|
383
|
+
value: "root",
|
|
384
|
+
label: "",
|
|
385
|
+
nodeValue: data,
|
|
386
|
+
nodeKey: undefined,
|
|
387
|
+
path: [],
|
|
388
|
+
isArrayItem: false,
|
|
389
|
+
isRoot: true,
|
|
390
|
+
children: children.length > 0 ? children : undefined,
|
|
391
|
+
};
|
|
392
|
+
return [rootNode];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// For primitives, just show the value directly
|
|
396
|
+
const node = buildTreeNodes(data, [], undefined, false, maxDepth);
|
|
397
|
+
return node ? [node] : [];
|
|
398
|
+
}, [data, maxDepth]);
|
|
399
|
+
|
|
400
|
+
// Compute initial expanded state (root is always expanded by default unless depth is 0)
|
|
401
|
+
const initialExpandedState = useMemo(() => {
|
|
402
|
+
if (defaultExpandedDepth === 0) return {};
|
|
403
|
+
if (defaultExpandedDepth === Infinity) {
|
|
404
|
+
return getTreeExpandedState(treeData, "*");
|
|
405
|
+
}
|
|
406
|
+
// Add 1 to depth to account for root node
|
|
407
|
+
const ids = getExpandedIds(treeData, defaultExpandedDepth + 1);
|
|
408
|
+
return getTreeExpandedState(treeData, ids);
|
|
409
|
+
}, [treeData, defaultExpandedDepth]);
|
|
410
|
+
|
|
411
|
+
const tree = useTree({ initialExpandedState });
|
|
412
|
+
|
|
413
|
+
// Render value based on type
|
|
414
|
+
const renderValue = useCallback(
|
|
415
|
+
(val: any, key: string | undefined, path: string[]): ReactNode => {
|
|
416
|
+
const custom = formatValue?.(key, val, path);
|
|
417
|
+
if (custom !== undefined) {
|
|
418
|
+
return (
|
|
419
|
+
<Text
|
|
420
|
+
component="span"
|
|
421
|
+
size={size}
|
|
422
|
+
style={STYLES.string}
|
|
423
|
+
className="alepha-json-viewer-value"
|
|
424
|
+
title={String(val)}
|
|
425
|
+
>
|
|
426
|
+
{custom}
|
|
427
|
+
</Text>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const type = getValueType(val);
|
|
432
|
+
switch (type) {
|
|
433
|
+
case "string": {
|
|
434
|
+
return (
|
|
435
|
+
<Text
|
|
436
|
+
style={STYLES.string}
|
|
437
|
+
component="span"
|
|
438
|
+
size={size}
|
|
439
|
+
className="alepha-json-viewer-value"
|
|
440
|
+
title={val}
|
|
441
|
+
>
|
|
442
|
+
"{val}"
|
|
443
|
+
</Text>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
case "number":
|
|
447
|
+
return (
|
|
448
|
+
<Text component="span" size={size} style={STYLES.number}>
|
|
449
|
+
{val}
|
|
450
|
+
</Text>
|
|
451
|
+
);
|
|
452
|
+
case "boolean":
|
|
453
|
+
return (
|
|
454
|
+
<Text component="span" size={size} style={STYLES.boolean}>
|
|
455
|
+
{String(val)}
|
|
456
|
+
</Text>
|
|
457
|
+
);
|
|
458
|
+
case "null":
|
|
459
|
+
case "undefined":
|
|
460
|
+
return (
|
|
461
|
+
<Text component="span" size={size} style={STYLES.null}>
|
|
462
|
+
{type}
|
|
463
|
+
</Text>
|
|
464
|
+
);
|
|
465
|
+
default:
|
|
466
|
+
return (
|
|
467
|
+
<Text component="span" size={size}>
|
|
468
|
+
{String(val)}
|
|
469
|
+
</Text>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
[formatValue, showQuotes, size],
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Render tree node
|
|
477
|
+
const renderNode = useCallback(
|
|
478
|
+
({
|
|
479
|
+
node,
|
|
480
|
+
expanded,
|
|
481
|
+
hasChildren,
|
|
482
|
+
elementProps,
|
|
483
|
+
}: {
|
|
484
|
+
node: JsonTreeNode;
|
|
485
|
+
expanded: boolean;
|
|
486
|
+
hasChildren: boolean;
|
|
487
|
+
elementProps: any;
|
|
488
|
+
}): ReactNode => {
|
|
489
|
+
return (
|
|
490
|
+
<RowNode
|
|
491
|
+
node={node}
|
|
492
|
+
expanded={expanded}
|
|
493
|
+
hasChildren={hasChildren}
|
|
494
|
+
elementProps={elementProps}
|
|
495
|
+
size={size}
|
|
496
|
+
config={config}
|
|
497
|
+
showQuotes={showQuotes}
|
|
498
|
+
showCopyButton={showCopyButton}
|
|
499
|
+
renderValue={renderValue}
|
|
500
|
+
/>
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
[config, renderValue, showCopyButton, showQuotes, size],
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (treeData.length === 0) {
|
|
507
|
+
return (
|
|
508
|
+
<Text size={size} style={STYLES.null}>
|
|
509
|
+
{data === null ? "null" : data === undefined ? "undefined" : "{}"}
|
|
510
|
+
</Text>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<Tree
|
|
516
|
+
data={treeData}
|
|
517
|
+
tree={tree}
|
|
518
|
+
levelOffset={config.levelOffset}
|
|
519
|
+
expandOnClick
|
|
520
|
+
renderNode={renderNode as any}
|
|
521
|
+
styles={{ root: STYLES.root }}
|
|
522
|
+
/>
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export default JsonViewer;
|