@alepha/ui 0.16.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
  2. package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
  4. package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
  7. package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
  8. package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
  9. package/dist/admin/{AdminLayout-D8yZ-8lG.js → AdminLayout-CsjvpeD1.js} +6 -10
  10. package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
  11. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
  12. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
  13. package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
  14. package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
  15. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
  16. package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
  17. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
  18. package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
  19. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
  20. package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
  21. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
  22. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
  23. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
  24. package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
  25. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
  26. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
  27. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
  28. package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
  29. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
  30. package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
  31. package/dist/admin/index.d.ts +22 -10
  32. package/dist/admin/index.d.ts.map +1 -1
  33. package/dist/admin/index.js +47 -48
  34. package/dist/admin/index.js.map +1 -1
  35. package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
  36. package/dist/auth/{AuthLayout-mFOWbiSP.js → AuthLayout-CdJcrPs4.js} +2 -4
  37. package/dist/auth/AuthLayout-CdJcrPs4.js.map +1 -0
  38. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  39. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  40. package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
  41. package/dist/auth/Login-DS_OqA0G.js.map +1 -0
  42. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
  43. package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
  44. package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
  45. package/dist/auth/Register-BRR2_gux.js.map +1 -0
  46. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
  47. package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
  48. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
  49. package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
  50. package/dist/auth/index.d.ts +14 -14
  51. package/dist/auth/index.d.ts.map +1 -1
  52. package/dist/auth/index.js +13 -17
  53. package/dist/auth/index.js.map +1 -1
  54. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  55. package/dist/core/index.d.ts +147 -68
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +349 -287
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
  60. package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
  61. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
  62. package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
  63. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
  64. package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
  65. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
  66. package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
  67. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
  68. package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
  69. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
  70. package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
  71. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
  72. package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
  73. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
  74. package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
  75. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
  76. package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
  77. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
  78. package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
  79. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
  80. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
  81. package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
  82. package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
  83. package/dist/demo/index.d.ts +2 -2
  84. package/dist/demo/index.d.ts.map +1 -1
  85. package/dist/demo/index.js +15 -15
  86. package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
  87. package/package.json +5 -3
  88. package/src/admin/AdminRouter.ts +15 -24
  89. package/src/admin/components/AdminLayout.tsx +6 -10
  90. package/src/admin/components/parameters/AdminParameters.tsx +154 -76
  91. package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
  92. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  93. package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
  94. package/src/admin/components/parameters/ParameterTree.tsx +280 -104
  95. package/src/admin/components/parameters/types.ts +3 -3
  96. package/src/admin/primitives/$uiAdmin.ts +2 -2
  97. package/src/auth/AuthRouter.ts +1 -4
  98. package/src/auth/components/AuthLayout.tsx +0 -1
  99. package/src/core/components/buttons/ActionButton.tsx +4 -15
  100. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  101. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  102. package/src/core/components/form/Control.tsx +10 -32
  103. package/src/core/components/form/ControlArray.tsx +200 -89
  104. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  105. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  106. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  107. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  108. package/src/core/components/layout/Sidebar.tsx +58 -18
  109. package/src/core/constants/ui.ts +1 -1
  110. package/src/core/helpers/renderIcon.tsx +5 -2
  111. package/src/core/index.ts +9 -5
  112. package/src/core/styles.css +7 -6
  113. package/src/core/utils/string.ts +28 -4
  114. package/src/demo/components/DemoLayout.tsx +6 -2
  115. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  116. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  117. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  118. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  119. package/dist/admin/AdminLayout-D8yZ-8lG.js.map +0 -1
  120. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  121. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  122. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  123. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  124. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  125. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  126. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  127. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  128. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  129. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  130. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  131. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  132. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  133. package/dist/auth/AuthLayout-mFOWbiSP.js.map +0 -1
  134. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  135. package/dist/auth/Login-CoU63mMR.js +0 -4
  136. package/dist/auth/Register-BV_oa_AK.js +0 -4
  137. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  138. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  139. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  140. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  141. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  142. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  143. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  144. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  145. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  146. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  147. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  148. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  149. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  150. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  151. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  152. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { ActionButton, Flex, Text } from "@alepha/ui";
2
2
  import {
3
3
  Badge,
4
- Card,
4
+ Box,
5
5
  Group,
6
6
  Loader,
7
7
  ScrollArea,
@@ -21,7 +21,6 @@ export interface ParameterHistoryProps {
21
21
  }
22
22
 
23
23
  const ParameterHistory = ({
24
- selectedConfig,
25
24
  history,
26
25
  loading,
27
26
  onRollback,
@@ -29,16 +28,6 @@ const ParameterHistory = ({
29
28
  const { l } = useI18n();
30
29
 
31
30
  const renderContent = () => {
32
- if (!selectedConfig) {
33
- return (
34
- <Flex flex={1} justify="center" align="center">
35
- <Text c="dimmed" size="xs">
36
- Select a parameter
37
- </Text>
38
- </Flex>
39
- );
40
- }
41
-
42
31
  if (loading) {
43
32
  return (
44
33
  <Flex flex={1} justify="center" align="center">
@@ -51,7 +40,7 @@ const ParameterHistory = ({
51
40
  return (
52
41
  <Flex flex={1} justify="center" align="center">
53
42
  <Text c="dimmed" size="xs">
54
- No history
43
+ Empty
55
44
  </Text>
56
45
  </Flex>
57
46
  );
@@ -124,22 +113,28 @@ const ParameterHistory = ({
124
113
  };
125
114
 
126
115
  return (
127
- <Card
128
- withBorder
129
- w={300}
116
+ <Box
117
+ w={160}
130
118
  h="100%"
131
- style={{ flexShrink: 0, overflow: "hidden" }}
119
+ p="xs"
120
+ style={{
121
+ flexShrink: 0,
122
+ overflow: "hidden",
123
+ display: "flex",
124
+ flexDirection: "column",
125
+ borderLeft: "1px solid var(--mantine-color-default-border)",
126
+ }}
132
127
  >
133
- <Stack gap="xs" h="100%">
128
+ <Stack gap="xs" h="100%" style={{ minHeight: 0 }}>
134
129
  <Group gap="xs">
135
130
  <IconHistory size={16} color="var(--mantine-color-dimmed)" />
136
131
  <Text size="sm" fw={500}>
137
- Version History
132
+ History
138
133
  </Text>
139
134
  </Group>
140
135
  {renderContent()}
141
136
  </Stack>
142
- </Card>
137
+ </Box>
143
138
  );
144
139
  };
145
140
 
@@ -1,14 +1,12 @@
1
1
  import { ActionButton, Text } from "@alepha/ui";
2
2
  import {
3
3
  Box,
4
- Card,
4
+ Collapse,
5
5
  Group,
6
6
  ScrollArea,
7
7
  Stack,
8
- Tooltip,
9
- Tree,
10
- type TreeNodeData,
11
- useTree,
8
+ TextInput,
9
+ UnstyledButton,
12
10
  } from "@mantine/core";
13
11
  import {
14
12
  IconChevronDown,
@@ -16,130 +14,308 @@ import {
16
14
  IconFolder,
17
15
  IconFolderOpen,
18
16
  IconRefresh,
17
+ IconSearch,
19
18
  IconSettings,
20
19
  } from "@tabler/icons-react";
21
- import type { ConfigTreeNode } from "alepha/api/parameters";
22
- import { type HTMLAttributes, useMemo } from "react";
20
+ import type { ParameterTreeNode } from "alepha/api/parameters";
21
+ import { memo, useCallback, useMemo, useState } from "react";
23
22
 
24
23
  export interface ParameterTreeProps {
25
- treeData: ConfigTreeNode[];
24
+ treeData: ParameterTreeNode[];
26
25
  selectedConfig: string | null;
27
26
  onSelect: (name: string) => void;
28
27
  onRefresh: () => void;
29
28
  }
30
29
 
31
- const ParameterTree = ({
32
- treeData,
33
- selectedConfig,
34
- onSelect,
35
- onRefresh,
36
- }: ParameterTreeProps) => {
37
- const tree = useTree({
38
- initialExpandedState: {},
39
- });
40
-
41
- const mantineTreeData = useMemo((): TreeNodeData[] => {
42
- const convert = (nodes: ConfigTreeNode[]): TreeNodeData[] => {
43
- return nodes.map((node) => ({
44
- value: node.path,
45
- label: node.name,
46
- children: node.children.length > 0 ? convert(node.children) : undefined,
47
- }));
48
- };
49
- return convert(treeData);
50
- }, [treeData]);
51
-
52
- const renderNode = ({
30
+ /**
31
+ * Filters tree nodes by search query.
32
+ */
33
+ const filterTree = (
34
+ nodes: ParameterTreeNode[],
35
+ query: string,
36
+ ): ParameterTreeNode[] => {
37
+ if (!query.trim()) return nodes;
38
+
39
+ const lowerQuery = query.toLowerCase();
40
+
41
+ return nodes
42
+ .map((node) => {
43
+ const filteredChildren = filterTree(node.children, query);
44
+ const nameMatches = node.name.toLowerCase().includes(lowerQuery);
45
+ const pathMatches = node.path.toLowerCase().includes(lowerQuery);
46
+
47
+ if (nameMatches || pathMatches || filteredChildren.length > 0) {
48
+ return {
49
+ ...node,
50
+ children: filteredChildren,
51
+ };
52
+ }
53
+
54
+ return null;
55
+ })
56
+ .filter((node): node is ParameterTreeNode => node !== null);
57
+ };
58
+
59
+ interface TreeNodeProps {
60
+ node: ParameterTreeNode;
61
+ level: number;
62
+ selectedConfig: string | null;
63
+ onSelect: (name: string) => void;
64
+ expandedNodes: Set<string>;
65
+ onToggle: (path: string) => void;
66
+ }
67
+
68
+ /**
69
+ * Memoized tree node to prevent unnecessary re-renders.
70
+ */
71
+ const TreeNode = memo(
72
+ ({
53
73
  node,
54
- expanded,
55
- hasChildren,
56
- elementProps,
57
- }: {
58
- node: TreeNodeData;
59
- expanded: boolean;
60
- hasChildren: boolean;
61
- elementProps: HTMLAttributes<HTMLDivElement>;
62
- }) => {
74
+ level,
75
+ selectedConfig,
76
+ onSelect,
77
+ expandedNodes,
78
+ onToggle,
79
+ }: TreeNodeProps) => {
80
+ const [isHovered, setIsHovered] = useState(false);
81
+ const hasChildren = node.children.length > 0;
82
+ const isExpanded = expandedNodes.has(node.path);
83
+ const isSelected = selectedConfig === node.path;
63
84
  const isLeaf = !hasChildren;
64
- const isSelected = selectedConfig === node.value;
85
+
86
+ const handleClick = useCallback(() => {
87
+ if (hasChildren) {
88
+ onToggle(node.path);
89
+ } else {
90
+ onSelect(node.path);
91
+ }
92
+ }, [hasChildren, node.path, onToggle, onSelect]);
93
+
94
+ const handleMouseEnter = useCallback(() => setIsHovered(true), []);
95
+ const handleMouseLeave = useCallback(() => setIsHovered(false), []);
65
96
 
66
97
  return (
67
- <Group
68
- gap="xs"
69
- wrap="nowrap"
70
- {...elementProps}
71
- onClick={(e) => {
72
- elementProps.onClick?.(e);
73
- if (isLeaf) {
74
- onSelect(node.value);
75
- }
76
- }}
77
- style={{
78
- ...elementProps.style,
79
- cursor: isLeaf ? "pointer" : "default",
80
- backgroundColor: isSelected
81
- ? "var(--mantine-color-blue-light)"
82
- : undefined,
83
- borderRadius: "var(--mantine-radius-sm)",
84
- paddingTop: 4,
85
- paddingBottom: 4,
86
- paddingRight: 8,
87
- }}
88
- >
89
- {hasChildren ? (
90
- <>
91
- {expanded ? (
92
- <IconChevronDown size={14} color="var(--mantine-color-dimmed)" />
93
- ) : (
94
- <IconChevronRight size={14} color="var(--mantine-color-dimmed)" />
95
- )}
96
- {expanded ? (
97
- <IconFolderOpen size={16} color="var(--mantine-color-blue-6)" />
98
+ <Box>
99
+ <UnstyledButton
100
+ onClick={handleClick}
101
+ onMouseEnter={handleMouseEnter}
102
+ onMouseLeave={handleMouseLeave}
103
+ w="100%"
104
+ style={{ display: "block" }}
105
+ >
106
+ <Group
107
+ gap={6}
108
+ wrap="nowrap"
109
+ p="4px 8px"
110
+ pl={8 + level * 16}
111
+ style={{
112
+ borderRadius: "var(--mantine-radius-sm)",
113
+ backgroundColor:
114
+ isSelected || isHovered
115
+ ? "var(--mantine-color-default-hover)"
116
+ : undefined,
117
+ }}
118
+ >
119
+ {hasChildren ? (
120
+ <>
121
+ <Box
122
+ style={{
123
+ display: "flex",
124
+ alignItems: "center",
125
+ justifyContent: "center",
126
+ width: 16,
127
+ }}
128
+ >
129
+ {isExpanded ? (
130
+ <IconChevronDown
131
+ size={14}
132
+ color="var(--mantine-color-dimmed)"
133
+ />
134
+ ) : (
135
+ <IconChevronRight
136
+ size={14}
137
+ color="var(--mantine-color-dimmed)"
138
+ />
139
+ )}
140
+ </Box>
141
+ {isExpanded ? (
142
+ <IconFolderOpen
143
+ size={16}
144
+ color="var(--mantine-color-dimmed)"
145
+ style={{ flexShrink: 0 }}
146
+ />
147
+ ) : (
148
+ <IconFolder
149
+ size={16}
150
+ color="var(--mantine-color-dimmed)"
151
+ style={{ flexShrink: 0 }}
152
+ />
153
+ )}
154
+ </>
98
155
  ) : (
99
- <IconFolder size={16} color="var(--mantine-color-blue-6)" />
156
+ <>
157
+ <Box w={16} />
158
+ <IconSettings
159
+ size={16}
160
+ color={
161
+ isSelected
162
+ ? "var(--mantine-color-blue-6)"
163
+ : "var(--mantine-color-dimmed)"
164
+ }
165
+ style={{ flexShrink: 0 }}
166
+ />
167
+ </>
100
168
  )}
101
- </>
102
- ) : (
103
- <>
104
- <Box w={14} />
105
- <IconSettings size={16} color="var(--mantine-color-gray-6)" />
106
- </>
169
+ <Text
170
+ size="sm"
171
+ fw={isSelected ? 600 : 400}
172
+ c={isSelected ? undefined : isLeaf ? undefined : "dimmed"}
173
+ style={{
174
+ whiteSpace: "nowrap",
175
+ overflow: "hidden",
176
+ textOverflow: "ellipsis",
177
+ }}
178
+ >
179
+ {node.name}
180
+ </Text>
181
+ </Group>
182
+ </UnstyledButton>
183
+
184
+ {hasChildren && (
185
+ <Collapse in={isExpanded}>
186
+ {node.children.map((child: ParameterTreeNode) => (
187
+ <TreeNode
188
+ key={child.path}
189
+ node={child}
190
+ level={level + 1}
191
+ selectedConfig={selectedConfig}
192
+ onSelect={onSelect}
193
+ expandedNodes={expandedNodes}
194
+ onToggle={onToggle}
195
+ />
196
+ ))}
197
+ </Collapse>
107
198
  )}
108
- <Text size="sm" fw={isSelected ? 500 : 400}>
109
- {node.label}
110
- </Text>
111
- </Group>
199
+ </Box>
112
200
  );
201
+ },
202
+ );
203
+
204
+ TreeNode.displayName = "TreeNode";
205
+
206
+ /**
207
+ * Collects all folder paths to expand by default.
208
+ */
209
+ const collectAllFolderPaths = (nodes: ParameterTreeNode[]): Set<string> => {
210
+ const paths = new Set<string>();
211
+
212
+ const traverse = (nodeList: ParameterTreeNode[]) => {
213
+ for (const node of nodeList) {
214
+ if (node.children.length > 0) {
215
+ paths.add(node.path);
216
+ traverse(node.children);
217
+ }
218
+ }
113
219
  };
114
220
 
221
+ traverse(nodes);
222
+ return paths;
223
+ };
224
+
225
+ const ParameterTree = ({
226
+ treeData,
227
+ selectedConfig,
228
+ onSelect,
229
+ onRefresh,
230
+ }: ParameterTreeProps) => {
231
+ const [searchQuery, setSearchQuery] = useState("");
232
+ const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() =>
233
+ collectAllFolderPaths(treeData),
234
+ );
235
+
236
+ // Filter tree by search query
237
+ const filteredTreeData = useMemo(
238
+ () => filterTree(treeData, searchQuery),
239
+ [treeData, searchQuery],
240
+ );
241
+
242
+ const handleToggle = useCallback((path: string) => {
243
+ setExpandedNodes((prev) => {
244
+ const next = new Set(prev);
245
+ if (next.has(path)) {
246
+ next.delete(path);
247
+ } else {
248
+ next.add(path);
249
+ }
250
+ return next;
251
+ });
252
+ }, []);
253
+
254
+ const handleSearchChange = useCallback(
255
+ (e: React.ChangeEvent<HTMLInputElement>) => {
256
+ setSearchQuery(e.currentTarget.value);
257
+ },
258
+ [],
259
+ );
260
+
115
261
  return (
116
- <Card withBorder w={280} h="100%" style={{ flexShrink: 0 }}>
117
- <Stack gap="xs" h="100%">
118
- <Group justify="space-between">
119
- <Text size="sm" fw={500}>
262
+ <Box
263
+ w={280}
264
+ h="100%"
265
+ p="sm"
266
+ style={{
267
+ flexShrink: 0,
268
+ display: "flex",
269
+ flexDirection: "column",
270
+ borderRight: "1px solid var(--mantine-color-default-border)",
271
+ }}
272
+ >
273
+ <Stack gap="sm" h="100%" style={{ minHeight: 0 }}>
274
+ <Group justify="space-between" gap="xs">
275
+ <Text size="sm" fw={600}>
120
276
  Parameters
121
277
  </Text>
122
- <Tooltip label="Refresh">
123
- <ActionButton
124
- variant="subtle"
125
- size="compact-xs"
126
- onClick={onRefresh}
127
- >
128
- <IconRefresh size={14} />
129
- </ActionButton>
130
- </Tooltip>
278
+ <ActionButton
279
+ variant="subtle"
280
+ size="compact-xs"
281
+ onClick={onRefresh}
282
+ tooltip="Refresh"
283
+ >
284
+ <IconRefresh size={14} />
285
+ </ActionButton>
131
286
  </Group>
132
- <ScrollArea flex={1} offsetScrollbars>
133
- <Tree
134
- data={mantineTreeData}
135
- tree={tree}
136
- levelOffset={20}
137
- expandOnClick
138
- renderNode={renderNode}
139
- />
287
+
288
+ <TextInput
289
+ placeholder="Search..."
290
+ size="xs"
291
+ leftSection={<IconSearch size={14} />}
292
+ value={searchQuery}
293
+ onChange={handleSearchChange}
294
+ />
295
+
296
+ <ScrollArea flex={1} offsetScrollbars style={{ minHeight: 0 }}>
297
+ {filteredTreeData.length === 0 ? (
298
+ <Text size="xs" c="dimmed" ta="center" py="md">
299
+ {searchQuery ? "No matching parameters" : "No parameters"}
300
+ </Text>
301
+ ) : (
302
+ <Stack gap={0} style={{ gap: 1 }}>
303
+ {filteredTreeData.map((node) => (
304
+ <TreeNode
305
+ key={node.path}
306
+ node={node}
307
+ level={0}
308
+ selectedConfig={selectedConfig}
309
+ onSelect={onSelect}
310
+ expandedNodes={expandedNodes}
311
+ onToggle={handleToggle}
312
+ />
313
+ ))}
314
+ </Stack>
315
+ )}
140
316
  </ScrollArea>
141
317
  </Stack>
142
- </Card>
318
+ </Box>
143
319
  );
144
320
  };
145
321
 
@@ -1,10 +1,10 @@
1
1
  import type { Parameter } from "alepha/api/parameters";
2
2
 
3
- export interface ConfigValue {
3
+ export interface ParameterValue {
4
4
  current?: Parameter;
5
5
  next?: Parameter;
6
6
  /**
7
- * Default value from the registered $config primitive.
7
+ * Default value from the registered $parameter primitive.
8
8
  */
9
9
  defaultValue?: unknown;
10
10
  /**
@@ -12,7 +12,7 @@ export interface ConfigValue {
12
12
  */
13
13
  currentValue?: unknown;
14
14
  /**
15
- * TypeBox/JSON schema for the configuration (as JSON from API).
15
+ * TypeBox/JSON schema for the parameter (as JSON from API).
16
16
  */
17
17
  schema?: Record<string, unknown>;
18
18
  }
@@ -1,11 +1,11 @@
1
- import type { AdminShellProps } from "@alepha/ui";
1
+ import type { DashboardShellProps } from "@alepha/ui";
2
2
  import { $context } from "alepha";
3
3
  import { AdminRouter } from "../AdminRouter.ts";
4
4
 
5
5
  /**
6
6
  * Register Admin UI components and get the AdminRouter instance.
7
7
  */
8
- export const $uiAdmin = (optsFn?: (a: AdminRouter) => AdminShellProps) => {
8
+ export const $uiAdmin = (optsFn?: (a: AdminRouter) => DashboardShellProps) => {
9
9
  const { alepha } = $context();
10
10
  const adminRouter = alepha.inject(AdminRouter);
11
11
 
@@ -27,6 +27,7 @@ export class AuthRouter {
27
27
  protected readonly auth = $inject(ReactAuth);
28
28
 
29
29
  authLayout = $page({
30
+ label: "Auth",
30
31
  path: "/auth",
31
32
  lazy: () => import("./components/AuthLayout.tsx"),
32
33
  children: () => [
@@ -46,7 +47,6 @@ export class AuthRouter {
46
47
  schema: {
47
48
  query: realmQuerySchema,
48
49
  },
49
- can: () => !this.auth.user,
50
50
  lazy: () => import("./components/Login.tsx"),
51
51
  loader: async ({ query, user }) => {
52
52
  if (user) {
@@ -66,7 +66,6 @@ export class AuthRouter {
66
66
  schema: {
67
67
  query: realmQuerySchema,
68
68
  },
69
- can: () => !this.auth.user,
70
69
  lazy: () => import("./components/Register.tsx"),
71
70
  loader: async ({ query, user }) => {
72
71
  if (user) {
@@ -86,7 +85,6 @@ export class AuthRouter {
86
85
  schema: {
87
86
  query: realmQuerySchema,
88
87
  },
89
- can: () => !this.auth.user,
90
88
  lazy: () => import("./components/ResetPassword.tsx"),
91
89
  loader: async ({ query, user }) => {
92
90
  if (user) {
@@ -116,7 +114,6 @@ export class AuthRouter {
116
114
  icon: IconLogout2,
117
115
  label: "Sign Out",
118
116
  description: "Sign out of your account",
119
- can: () => !!this.auth.user,
120
117
  path: "/logout",
121
118
  component: () => null,
122
119
  loader: () => {
@@ -1,7 +1,6 @@
1
1
  import { AlephaMantineProvider } from "@alepha/ui";
2
2
  import { Flex } from "@mantine/core";
3
3
  import { NestedView } from "alepha/react/router";
4
- import "@alepha/ui/styles";
5
4
 
6
5
  const AuthLayout = () => {
7
6
  return (
@@ -243,17 +243,14 @@ const ActionButton = (_props: ActionProps) => {
243
243
  const props = { ..._props };
244
244
  const { tooltip, menu, icon, ...restProps } = props;
245
245
 
246
- if (props.variant === "subtle") {
247
- restProps.c ??= "var(--mantine-color-text)";
246
+ if (props.variant === "subtle" || props.variant === "outline") {
248
247
  restProps.color ??= "gray";
249
248
  }
250
249
 
251
250
  if (props.intent) {
252
251
  if (props.intent === "none") {
253
- restProps.c ??= "var(--mantine-color-text)";
254
252
  restProps.color ??= "gray";
255
253
  } else if (props.intent === "primary") {
256
- restProps.c ??= "white";
257
254
  restProps.color ??= theme.primaryColor;
258
255
  } else if (props.intent === "success") {
259
256
  restProps.c ??= "white";
@@ -262,7 +259,6 @@ const ActionButton = (_props: ActionProps) => {
262
259
  restProps.c ??= "white";
263
260
  restProps.color ??= "red";
264
261
  } else if (props.intent === "warning") {
265
- restProps.c ??= "var(--mantine-color-text)";
266
262
  restProps.color ??= "yellow";
267
263
  } else if (props.intent === "info") {
268
264
  restProps.c ??= "white";
@@ -271,18 +267,11 @@ const ActionButton = (_props: ActionProps) => {
271
267
  }
272
268
 
273
269
  if (props.icon) {
270
+ const sizes = ui.sizes.icon as Record<string, number>;
274
271
  const icon = isComponentType(props.icon) ? (
275
- <props.icon size={ui.sizes.icon.md} />
272
+ <props.icon size={sizes[props.size || "md"]} />
276
273
  ) : (
277
- <ThemeIcon
278
- w={24} // TODO: make size configurable
279
- variant={"transparent"}
280
- size={"sm"}
281
- c={"var(--mantine-color-text)"}
282
- {...props.themeIconProps}
283
- >
284
- {props.icon as ReactNode}
285
- </ThemeIcon>
274
+ <span>{props.icon as ReactNode}</span>
286
275
  );
287
276
 
288
277
  if (!props.children) {
@@ -1,5 +1,6 @@
1
1
  import { useMantineColorScheme } from "@mantine/core";
2
2
  import { IconMoon, IconSun } from "@tabler/icons-react";
3
+ import { ui } from "../../constants/ui.ts";
3
4
  import ActionButton, { type ActionProps } from "./ActionButton.tsx";
4
5
 
5
6
  /**
@@ -19,17 +20,20 @@ const DarkModeButton = (props: Partial<ActionProps>) => {
19
20
  setColorScheme(current === "dark" ? "light" : "dark");
20
21
  };
21
22
 
23
+ const size = props.size ?? "md";
24
+ const iconSize =
25
+ (ui.sizes.icon as Record<string, number>)[size] ?? ui.sizes.icon.md;
26
+
22
27
  return (
23
28
  <ActionButton
24
29
  onClick={toggleColorScheme}
25
30
  variant={props.variant ?? "subtle"}
26
- size={props.size ?? "sm"}
31
+ size={size}
27
32
  aria-label="Toggle color scheme"
28
- px={"xs"}
29
33
  icon={
30
34
  <>
31
- <IconSun className="alepha-light-hidden" />
32
- <IconMoon className="alepha-dark-hidden" />
35
+ <IconSun size={iconSize} className="alepha-light-hidden" />
36
+ <IconMoon size={iconSize} className="alepha-dark-hidden" />
33
37
  </>
34
38
  }
35
39
  {...props}
@@ -14,11 +14,9 @@ const ToggleSidebarButton = (props: Props) => {
14
14
  return (
15
15
  <ActionButton
16
16
  icon={
17
- sidebar.collapsed ? (
18
- <IconLayoutSidebarRightCollapse />
19
- ) : (
20
- <IconLayoutSidebarLeftCollapse />
21
- )
17
+ sidebar.collapsed
18
+ ? IconLayoutSidebarRightCollapse
19
+ : IconLayoutSidebarLeftCollapse
22
20
  }
23
21
  visibleFrom={"sm"}
24
22
  variant={"subtle"}