@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.
Files changed (183) hide show
  1. package/dist/admin/AdminAudits-B3EhKhN7.js +3 -0
  2. package/dist/admin/{AdminAudits-CwvH8e8c.js → AdminAudits-DIrCCPk3.js} +3 -2
  3. package/dist/admin/AdminAudits-DIrCCPk3.js.map +1 -0
  4. package/dist/admin/AdminFiles-C8OG4dtD.js +3 -0
  5. package/dist/admin/{AdminFiles-C_w1tb_x.js → AdminFiles-RsL178Ta.js} +2 -2
  6. package/dist/admin/{AdminFiles-C_w1tb_x.js.map → AdminFiles-RsL178Ta.js.map} +1 -1
  7. package/dist/admin/AdminNotifications-BSL4B2fQ.js +3 -0
  8. package/dist/admin/{AdminNotifications-DuYy74AN.js → AdminNotifications-cIbywWKi.js} +2 -2
  9. package/dist/admin/AdminNotifications-cIbywWKi.js.map +1 -0
  10. package/dist/admin/{AdminParameters-DYg48Jwe.js → AdminParameters-BKObzzpN.js} +1 -1
  11. package/dist/admin/{AdminParameters-YagqWTG3.js → AdminParameters-D-q3Qmhv.js} +2 -2
  12. package/dist/admin/{AdminParameters-YagqWTG3.js.map → AdminParameters-D-q3Qmhv.js.map} +1 -1
  13. package/dist/admin/AdminSessions-DHG9zPfr.js +3 -0
  14. package/dist/admin/{AdminSessions-BCjgJ-93.js → AdminSessions-vOgkrQ2U.js} +3 -2
  15. package/dist/admin/AdminSessions-vOgkrQ2U.js.map +1 -0
  16. package/dist/admin/{AdminUserAudits-B_PUXCKC.js → AdminUserAudits-CSsN1fIC.js} +3 -2
  17. package/dist/admin/AdminUserAudits-CSsN1fIC.js.map +1 -0
  18. package/dist/admin/{AdminUserAudits-D7cTcElL.js → AdminUserAudits-DmAnivo3.js} +1 -1
  19. package/dist/admin/{AdminUserCreate-DzfRbGZ4.js → AdminUserCreate-B72nu-3W.js} +3 -2
  20. package/dist/admin/AdminUserCreate-B72nu-3W.js.map +1 -0
  21. package/dist/admin/{AdminUserCreate-oUA1KDIl.js → AdminUserCreate-DpA13zwj.js} +1 -1
  22. package/dist/admin/AdminUserDetails-CKM2IEMr.js +475 -0
  23. package/dist/admin/AdminUserDetails-CKM2IEMr.js.map +1 -0
  24. package/dist/admin/{AdminUserDetails-y1H5DW8Y.js → AdminUserDetails-Zib_B6Al.js} +1 -1
  25. package/dist/admin/{AdminUserLayout-Dejnz13m.js → AdminUserLayout-BNBOEiAO.js} +1 -1
  26. package/dist/admin/AdminUserLayout-D7En9UBq.js +334 -0
  27. package/dist/admin/AdminUserLayout-D7En9UBq.js.map +1 -0
  28. package/dist/admin/AdminUserSessions-D9X2_HMA.js +3 -0
  29. package/dist/admin/{AdminUserSessions-DO9H85O-.js → AdminUserSessions-DEaGu6n6.js} +3 -2
  30. package/dist/admin/AdminUserSessions-DEaGu6n6.js.map +1 -0
  31. package/dist/admin/{AdminUserSettings-B3jA8g3p.js → AdminUserSettings-Di73D7g2.js} +8 -6
  32. package/dist/admin/AdminUserSettings-Di73D7g2.js.map +1 -0
  33. package/dist/admin/AdminUserSettings-yI-JECf5.js +3 -0
  34. package/dist/admin/{AdminUsers-ebbrJBT0.js → AdminUsers-BnGIRvmV.js} +3 -2
  35. package/dist/admin/AdminUsers-BnGIRvmV.js.map +1 -0
  36. package/dist/admin/AdminUsers-CG9-2Z8W.js +3 -0
  37. package/dist/admin/index.d.ts +25 -25
  38. package/dist/admin/index.d.ts.map +1 -1
  39. package/dist/admin/index.js +37 -36
  40. package/dist/admin/index.js.map +1 -1
  41. package/dist/auth/{AuthLayout-BAZJHzDG.js → AuthLayout-B1sUB8fB.js} +2 -2
  42. package/dist/auth/AuthLayout-B1sUB8fB.js.map +1 -0
  43. package/dist/auth/Login-BWi-pPbO.js +4 -0
  44. package/dist/auth/{Login-CeNZZjrr.js → Login-Cjxv3EDi.js} +2 -2
  45. package/dist/auth/Login-Cjxv3EDi.js.map +1 -0
  46. package/dist/auth/{Register-s4ENeyiE.js → Register-BKBIpHhW.js} +3 -2
  47. package/dist/auth/Register-BKBIpHhW.js.map +1 -0
  48. package/dist/auth/Register-CtdvihIM.js +4 -0
  49. package/dist/auth/ResetPassword-BUdM7T_R.js +3 -0
  50. package/dist/auth/{ResetPassword-GLIFkJT7.js → ResetPassword-DvqD_1SJ.js} +3 -2
  51. package/dist/auth/ResetPassword-DvqD_1SJ.js.map +1 -0
  52. package/dist/auth/VerifyEmail-BYmtnkEl.js +3 -0
  53. package/dist/auth/{VerifyEmail-R79sSej_.js → VerifyEmail-VaBruOnO.js} +3 -2
  54. package/dist/auth/VerifyEmail-VaBruOnO.js.map +1 -0
  55. package/dist/auth/index.d.ts +11 -11
  56. package/dist/auth/index.d.ts.map +1 -1
  57. package/dist/auth/index.js +10 -10
  58. package/dist/auth/index.js.map +1 -1
  59. package/dist/core/index.d.ts +36 -55
  60. package/dist/core/index.d.ts.map +1 -1
  61. package/dist/core/index.js +50 -350
  62. package/dist/core/index.js.map +1 -1
  63. package/dist/demo/DemoDataTable-2mzzf__a.js +150 -0
  64. package/dist/demo/DemoDataTable-2mzzf__a.js.map +1 -0
  65. package/dist/demo/DemoHome-CnuL5WV9.js +25 -0
  66. package/dist/demo/DemoHome-CnuL5WV9.js.map +1 -0
  67. package/dist/demo/DemoHome-D6Z7EE4V.js +3 -0
  68. package/dist/demo/DemoJsonViewer-CYUggLop.js +4 -0
  69. package/dist/demo/DemoJsonViewer-NUGst5wW.js +430 -0
  70. package/dist/demo/DemoJsonViewer-NUGst5wW.js.map +1 -0
  71. package/dist/demo/DemoLayout-ZFDzyvY3.js +3 -0
  72. package/dist/demo/DemoLayout-dvbeuBBf.js +47 -0
  73. package/dist/demo/DemoLayout-dvbeuBBf.js.map +1 -0
  74. package/dist/demo/DemoLogin--wE44i23.js +327 -0
  75. package/dist/demo/DemoLogin--wE44i23.js.map +1 -0
  76. package/dist/demo/DemoRegister-BtrMksx6.js +488 -0
  77. package/dist/demo/DemoRegister-BtrMksx6.js.map +1 -0
  78. package/dist/demo/DemoResetPassword-DVXiiiX7.js +341 -0
  79. package/dist/demo/DemoResetPassword-DVXiiiX7.js.map +1 -0
  80. package/dist/demo/DemoSidebar-DWnjYHoP.js +82 -0
  81. package/dist/demo/DemoSidebar-DWnjYHoP.js.map +1 -0
  82. package/dist/demo/DemoTypeForm-P5_VInW2.js +83 -0
  83. package/dist/demo/DemoTypeForm-P5_VInW2.js.map +1 -0
  84. package/dist/demo/DemoVerifyEmail-C_ooC5u8.js +152 -0
  85. package/dist/demo/DemoVerifyEmail-C_ooC5u8.js.map +1 -0
  86. package/dist/demo/IconGoogle-DvmFiEDB.js +58 -0
  87. package/dist/demo/IconGoogle-DvmFiEDB.js.map +1 -0
  88. package/dist/demo/Showcase-vemLuO2t.js +187 -0
  89. package/dist/demo/Showcase-vemLuO2t.js.map +1 -0
  90. package/dist/demo/index.d.ts +97 -0
  91. package/dist/demo/index.d.ts.map +1 -0
  92. package/dist/demo/index.js +121 -0
  93. package/dist/demo/index.js.map +1 -0
  94. package/dist/json/index.d.ts +58 -0
  95. package/dist/json/index.d.ts.map +1 -0
  96. package/dist/json/index.js +325 -0
  97. package/dist/json/index.js.map +1 -0
  98. package/package.json +25 -14
  99. package/src/admin/AdminRouter.ts +23 -20
  100. package/src/admin/MainRouter.ts +2 -2
  101. package/src/admin/components/audits/AdminAudits.tsx +4 -3
  102. package/src/admin/components/jobs/AdminJobs.tsx +2 -2
  103. package/src/admin/components/notifications/AdminNotifications.tsx +2 -2
  104. package/src/admin/components/parameters/AdminParameters.tsx +2 -2
  105. package/src/admin/components/sessions/AdminSessions.tsx +4 -3
  106. package/src/admin/components/shared/AdminResourceHeader.tsx +281 -0
  107. package/src/admin/components/shared/AdminResourceTabs.tsx +94 -0
  108. package/src/admin/components/shared/index.ts +10 -0
  109. package/src/admin/components/users/AdminUserAudits.tsx +4 -3
  110. package/src/admin/components/users/AdminUserCreate.tsx +4 -3
  111. package/src/admin/components/users/AdminUserDetails.tsx +339 -86
  112. package/src/admin/components/users/AdminUserLayout.tsx +165 -113
  113. package/src/admin/components/users/AdminUserSessions.tsx +4 -3
  114. package/src/admin/components/users/AdminUserSettings.tsx +12 -6
  115. package/src/admin/components/users/AdminUsers.tsx +8 -3
  116. package/src/auth/AuthRouter.ts +1 -1
  117. package/src/auth/components/AuthLayout.tsx +1 -1
  118. package/src/auth/components/Login.tsx +1 -1
  119. package/src/auth/components/Register.tsx +2 -1
  120. package/src/auth/components/ResetPassword.tsx +2 -1
  121. package/src/auth/components/VerifyEmail.tsx +2 -1
  122. package/src/auth/components/buttons/UserButton.tsx +1 -1
  123. package/src/core/RootRouter.ts +1 -1
  124. package/src/core/components/buttons/ActionButton.tsx +3 -4
  125. package/src/core/components/form/Control.tsx +12 -1
  126. package/src/core/components/form/ControlNumber.tsx +5 -0
  127. package/src/core/components/form/TypeForm.tsx +3 -2
  128. package/src/core/components/layout/AdminShell.tsx +2 -1
  129. package/src/core/components/layout/AlephaMantineProvider.tsx +7 -2
  130. package/src/core/components/layout/Omnibar.tsx +2 -1
  131. package/src/core/components/layout/Sidebar.tsx +18 -18
  132. package/src/core/index.ts +1 -2
  133. package/src/core/services/DialogService.tsx +0 -17
  134. package/{styles.css → src/core/styles.css} +1 -5
  135. package/src/demo/DemoRouter.ts +123 -0
  136. package/src/demo/components/DemoHome.tsx +29 -0
  137. package/src/demo/components/DemoLayout.tsx +52 -0
  138. package/src/demo/components/auth/DemoLogin.tsx +130 -0
  139. package/src/demo/components/auth/DemoRegister.tsx +144 -0
  140. package/src/demo/components/auth/DemoResetPassword.tsx +69 -0
  141. package/src/demo/components/auth/DemoVerifyEmail.tsx +28 -0
  142. package/src/demo/components/core/DemoDataTable.tsx +174 -0
  143. package/src/demo/components/core/DemoSidebar.tsx +85 -0
  144. package/src/demo/components/core/DemoTypeForm.tsx +69 -0
  145. package/src/demo/components/json/DemoJsonViewer.tsx +128 -0
  146. package/src/demo/components/shared/MacWindow.tsx +105 -0
  147. package/src/demo/components/shared/Showcase.tsx +112 -0
  148. package/src/demo/index.ts +30 -0
  149. package/src/demo/styles.css +0 -0
  150. package/src/json/components/JsonViewer.css +25 -0
  151. package/src/json/components/JsonViewer.tsx +526 -0
  152. package/src/json/extensions/DialogService.tsx +31 -0
  153. package/src/json/index.ts +5 -0
  154. package/src/json/styles.css +1 -0
  155. package/dist/admin/AdminAudits-CwvH8e8c.js.map +0 -1
  156. package/dist/admin/AdminAudits-Dv8Vk_6r.js +0 -3
  157. package/dist/admin/AdminFiles-5CPA3lQk.js +0 -3
  158. package/dist/admin/AdminNotifications-DLjmZWtf.js +0 -3
  159. package/dist/admin/AdminNotifications-DuYy74AN.js.map +0 -1
  160. package/dist/admin/AdminSessions-BCjgJ-93.js.map +0 -1
  161. package/dist/admin/AdminSessions-DEh2uN-4.js +0 -3
  162. package/dist/admin/AdminUserAudits-B_PUXCKC.js.map +0 -1
  163. package/dist/admin/AdminUserCreate-DzfRbGZ4.js.map +0 -1
  164. package/dist/admin/AdminUserDetails-DeTrJm-t.js +0 -221
  165. package/dist/admin/AdminUserDetails-DeTrJm-t.js.map +0 -1
  166. package/dist/admin/AdminUserLayout-CsfrrZkD.js +0 -150
  167. package/dist/admin/AdminUserLayout-CsfrrZkD.js.map +0 -1
  168. package/dist/admin/AdminUserSessions-Bbhcpz4k.js +0 -3
  169. package/dist/admin/AdminUserSessions-DO9H85O-.js.map +0 -1
  170. package/dist/admin/AdminUserSettings-B3jA8g3p.js.map +0 -1
  171. package/dist/admin/AdminUserSettings-CE0xpbQc.js +0 -3
  172. package/dist/admin/AdminUsers-CegGZDhW.js +0 -3
  173. package/dist/admin/AdminUsers-ebbrJBT0.js.map +0 -1
  174. package/dist/auth/AuthLayout-BAZJHzDG.js.map +0 -1
  175. package/dist/auth/Login-CeNZZjrr.js.map +0 -1
  176. package/dist/auth/Login-hQcu1nlu.js +0 -4
  177. package/dist/auth/Register-B6HBNVHS.js +0 -4
  178. package/dist/auth/Register-s4ENeyiE.js.map +0 -1
  179. package/dist/auth/ResetPassword-Cjd-W-Nu.js +0 -3
  180. package/dist/auth/ResetPassword-GLIFkJT7.js.map +0 -1
  181. package/dist/auth/VerifyEmail-Dc9ABKUw.js +0 -3
  182. package/dist/auth/VerifyEmail-R79sSej_.js.map +0 -1
  183. 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;