@alepha/ui 0.10.7 → 0.11.1
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/{AlephaMantineProvider-WfiC2EH6.js → AlephaMantineProvider-DDbIijPF.js} +5 -4
- package/dist/AlephaMantineProvider-DDbIijPF.js.map +1 -0
- package/dist/AlephaMantineProvider-pOu8hOzK.js +3 -0
- package/dist/index.d.ts +365 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +903 -111
- package/dist/index.js.map +1 -1
- package/package.json +24 -20
- package/src/components/Action.tsx +179 -18
- package/src/components/Control.tsx +15 -154
- package/src/components/ControlDate.tsx +2 -10
- package/src/components/ControlSelect.tsx +71 -9
- package/src/components/DarkModeButton.tsx +9 -2
- package/src/components/DataTable.css +199 -0
- package/src/components/DataTable.tsx +724 -0
- package/src/components/Omnibar.tsx +2 -1
- package/src/components/Sidebar.css +217 -0
- package/src/components/Sidebar.tsx +255 -0
- package/src/components/TypeForm.tsx +13 -13
- package/src/components/dialogs/AlertDialog.tsx +13 -0
- package/src/components/dialogs/ConfirmDialog.tsx +21 -0
- package/src/components/dialogs/PromptDialog.tsx +52 -0
- package/src/hooks/useDialog.ts +15 -0
- package/src/index.ts +30 -2
- package/src/services/DialogService.tsx +207 -0
- package/src/utils/parseInput.ts +125 -0
- package/dist/AlephaMantineProvider-EemOtraW.js +0 -3
- package/dist/AlephaMantineProvider-WfiC2EH6.js.map +0 -1
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
IconSettings,
|
|
8
8
|
IconUser,
|
|
9
9
|
} from "@tabler/icons-react";
|
|
10
|
-
|
|
10
|
+
// biome-ignore lint/correctness/noUnusedImports: required
|
|
11
|
+
import React, { type ReactNode } from "react";
|
|
11
12
|
|
|
12
13
|
export interface OmnibarProps {
|
|
13
14
|
actions?: SpotlightActionData[];
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/* Sidebar Component Styles */
|
|
2
|
+
|
|
3
|
+
.alepha-sidebar {
|
|
4
|
+
width: 100%;
|
|
5
|
+
padding: 8px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Search button styles */
|
|
9
|
+
.alepha-sidebar-search-button {
|
|
10
|
+
display: block;
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: 8px 12px;
|
|
13
|
+
margin-bottom: 12px;
|
|
14
|
+
background-color: var(--alepha-surface);
|
|
15
|
+
border: 1px solid var(--alepha-border);
|
|
16
|
+
border-radius: 8px;
|
|
17
|
+
transition: all 150ms ease;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.alepha-sidebar-search-button:hover {
|
|
23
|
+
background-color: var(--alepha-elevated-hover);
|
|
24
|
+
border-color: var(--mantine-color-blue-6);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.alepha-sidebar-search-button-content {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.alepha-sidebar-search-button-left {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 10px;
|
|
38
|
+
color: var(--alepha-text-muted);
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.alepha-sidebar-search-button-shortcut {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.alepha-sidebar-search-button-shortcut kbd {
|
|
49
|
+
padding: 2px 6px;
|
|
50
|
+
background-color: var(--alepha-elevated);
|
|
51
|
+
border: 1px solid var(--alepha-border);
|
|
52
|
+
border-radius: 4px;
|
|
53
|
+
font-size: 11px;
|
|
54
|
+
font-family: inherit;
|
|
55
|
+
color: var(--alepha-text-muted);
|
|
56
|
+
box-shadow: 0 1px 2px var(--alepha-shadow);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.alepha-sidebar-item-wrapper {
|
|
60
|
+
position: relative;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.alepha-sidebar-item {
|
|
64
|
+
display: block;
|
|
65
|
+
width: 100%;
|
|
66
|
+
padding: 8px 12px;
|
|
67
|
+
text-decoration: none;
|
|
68
|
+
color: var(--alepha-text);
|
|
69
|
+
border-radius: 24px;
|
|
70
|
+
transition: background-color 150ms ease;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
line-height: 1.5;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Full width for level 0 items */
|
|
77
|
+
.alepha-sidebar-level-0 {
|
|
78
|
+
margin-left: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Adjusted width for indented items */
|
|
82
|
+
.alepha-sidebar-level-1 {
|
|
83
|
+
width: calc(100% - 24px);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.alepha-sidebar-level-2 {
|
|
87
|
+
width: calc(100% - 48px);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Active state for current page */
|
|
91
|
+
.alepha-sidebar-item-active {
|
|
92
|
+
background-color: var(--mantine-color-blue-0);
|
|
93
|
+
color: var(--mantine-color-blue-7);
|
|
94
|
+
font-weight: 500;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.alepha-sidebar-item:hover:not(.alepha-sidebar-item-active) {
|
|
98
|
+
background-color: var(--alepha-elevated-hover);
|
|
99
|
+
color: var(--alepha-text) !important;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-mantine-color-scheme="dark"] .alepha-sidebar-item-active {
|
|
103
|
+
background-color: rgba(34, 139, 230, 0.15);
|
|
104
|
+
color: var(--mantine-color-blue-4);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.alepha-sidebar-item-active .alepha-sidebar-item-icon {
|
|
108
|
+
color: var(--mantine-color-blue-6);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[data-mantine-color-scheme="dark"]
|
|
112
|
+
.alepha-sidebar-item-active
|
|
113
|
+
.alepha-sidebar-item-icon {
|
|
114
|
+
color: var(--mantine-color-blue-4);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.alepha-sidebar-item-content {
|
|
118
|
+
flex: 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.alepha-sidebar-item-icon {
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
color: var(--alepha-text-muted);
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.alepha-sidebar-item-label {
|
|
130
|
+
flex: 1;
|
|
131
|
+
font-weight: 400;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.alepha-sidebar-item-caret {
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
color: var(--alepha-text-muted);
|
|
137
|
+
transition: transform 150ms ease;
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Level-specific indentation (now in the width calc above) */
|
|
144
|
+
.alepha-sidebar-level-1 {
|
|
145
|
+
margin-left: 24px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.alepha-sidebar-level-2 {
|
|
149
|
+
margin-left: 48px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Children container */
|
|
153
|
+
.alepha-sidebar-children {
|
|
154
|
+
position: relative;
|
|
155
|
+
margin-top: 2px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.alepha-sidebar-children-items {
|
|
159
|
+
position: relative;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Vertical bar for nested items */
|
|
163
|
+
.alepha-sidebar-vertical-bar {
|
|
164
|
+
position: absolute;
|
|
165
|
+
top: 0;
|
|
166
|
+
bottom: 0;
|
|
167
|
+
width: 1px;
|
|
168
|
+
background: linear-gradient(
|
|
169
|
+
to bottom,
|
|
170
|
+
var(--alepha-border) 0%,
|
|
171
|
+
var(--alepha-border) 90%,
|
|
172
|
+
transparent 100%
|
|
173
|
+
);
|
|
174
|
+
opacity: 0.5;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Vertical bar positioning based on parent level */
|
|
178
|
+
.alepha-sidebar-children[data-parent-level="0"] .alepha-sidebar-vertical-bar {
|
|
179
|
+
left: 20px; /* Position for level 1 children (level 2 items) */
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.alepha-sidebar-children[data-parent-level="1"] .alepha-sidebar-vertical-bar {
|
|
183
|
+
left: 45px; /* Position for level 2 children (level 3 items) */
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Smooth animations */
|
|
187
|
+
.alepha-sidebar-children {
|
|
188
|
+
animation: alepha-sidebar-slideDown 150ms ease-out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@keyframes alepha-sidebar-slideDown {
|
|
192
|
+
from {
|
|
193
|
+
opacity: 0;
|
|
194
|
+
transform: translateY(-4px);
|
|
195
|
+
}
|
|
196
|
+
to {
|
|
197
|
+
opacity: 1;
|
|
198
|
+
transform: translateY(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Remove default focus outline and add custom */
|
|
203
|
+
.alepha-sidebar-item:focus-visible {
|
|
204
|
+
outline: none;
|
|
205
|
+
box-shadow: 0 0 0 2px var(--alepha-focus-ring, #0969da);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Active state */
|
|
209
|
+
.alepha-sidebar-item:active {
|
|
210
|
+
transform: scale(0.98);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Optional: Add subtle hover effect for icons (but not for active items) */
|
|
214
|
+
.alepha-sidebar-item:hover:not(.alepha-sidebar-item-active)
|
|
215
|
+
.alepha-sidebar-item-icon {
|
|
216
|
+
color: var(--alepha-text);
|
|
217
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { useActive } from "@alepha/react";
|
|
2
|
+
import { Box, Flex, UnstyledButton } from "@mantine/core";
|
|
3
|
+
import {
|
|
4
|
+
IconChevronDown,
|
|
5
|
+
IconChevronRight,
|
|
6
|
+
IconCircle,
|
|
7
|
+
IconSearch,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
import { type FC, type ReactNode, useState } from "react";
|
|
10
|
+
|
|
11
|
+
export interface MenuItem {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
href?: string;
|
|
16
|
+
activeStartsWith?: boolean; // Use startWith matching for active state
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
children?: MenuItem[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SidebarProps {
|
|
22
|
+
menu: MenuItem[];
|
|
23
|
+
defaultOpenIds?: string[];
|
|
24
|
+
onItemClick?: (item: MenuItem) => void;
|
|
25
|
+
showSearchButton?: boolean;
|
|
26
|
+
onSearchClick?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Sidebar: FC<SidebarProps> = ({
|
|
30
|
+
menu,
|
|
31
|
+
defaultOpenIds = [],
|
|
32
|
+
onItemClick,
|
|
33
|
+
showSearchButton = false,
|
|
34
|
+
onSearchClick,
|
|
35
|
+
}) => {
|
|
36
|
+
const [openIds, setOpenIds] = useState<Set<string>>(new Set(defaultOpenIds));
|
|
37
|
+
|
|
38
|
+
const toggleOpen = (id: string) => {
|
|
39
|
+
setOpenIds((prev) => {
|
|
40
|
+
const newSet = new Set(prev);
|
|
41
|
+
if (newSet.has(id)) {
|
|
42
|
+
newSet.delete(id);
|
|
43
|
+
} else {
|
|
44
|
+
newSet.add(id);
|
|
45
|
+
}
|
|
46
|
+
return newSet;
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box component="nav" className="alepha-sidebar">
|
|
52
|
+
{showSearchButton && (
|
|
53
|
+
<UnstyledButton
|
|
54
|
+
className="alepha-sidebar-search-button"
|
|
55
|
+
onClick={onSearchClick}
|
|
56
|
+
>
|
|
57
|
+
<Box className="alepha-sidebar-search-button-content">
|
|
58
|
+
<Box className="alepha-sidebar-search-button-left">
|
|
59
|
+
<IconSearch size={16} />
|
|
60
|
+
<span>Search...</span>
|
|
61
|
+
</Box>
|
|
62
|
+
<Box className="alepha-sidebar-search-button-shortcut">
|
|
63
|
+
<kbd>⌘+K</kbd>
|
|
64
|
+
</Box>
|
|
65
|
+
</Box>
|
|
66
|
+
</UnstyledButton>
|
|
67
|
+
)}
|
|
68
|
+
{menu.map((item) => (
|
|
69
|
+
<SidebarItem
|
|
70
|
+
key={item.id}
|
|
71
|
+
item={item}
|
|
72
|
+
level={0}
|
|
73
|
+
openIds={openIds}
|
|
74
|
+
onToggle={toggleOpen}
|
|
75
|
+
onItemClick={onItemClick}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</Box>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
83
|
+
// SidebarItem - Main component that decides which variant to render
|
|
84
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export interface SidebarItemProps {
|
|
87
|
+
item: MenuItem;
|
|
88
|
+
level: number;
|
|
89
|
+
openIds: Set<string>;
|
|
90
|
+
onToggle: (id: string) => void;
|
|
91
|
+
onItemClick?: (item: MenuItem) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const SidebarItem: React.FC<SidebarItemProps> = (props) => {
|
|
95
|
+
const { item, level } = props;
|
|
96
|
+
const maxLevel = 2; // 0, 1, 2 = 3 levels total
|
|
97
|
+
|
|
98
|
+
if (level > maxLevel) return null;
|
|
99
|
+
|
|
100
|
+
// Render different components based on whether item has href or not
|
|
101
|
+
if (item.href) {
|
|
102
|
+
return <SidebarItemHref {...props} />;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return <SidebarItemButton {...props} />;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
109
|
+
// SidebarItemHref - Component for items with href (navigation)
|
|
110
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
const SidebarItemHref: React.FC<SidebarItemProps> = ({
|
|
113
|
+
item,
|
|
114
|
+
level,
|
|
115
|
+
openIds,
|
|
116
|
+
onToggle,
|
|
117
|
+
onItemClick,
|
|
118
|
+
}) => {
|
|
119
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
120
|
+
const isOpen = openIds.has(item.id);
|
|
121
|
+
|
|
122
|
+
// Use the useActive hook for navigation
|
|
123
|
+
const { isActive, anchorProps } = useActive({
|
|
124
|
+
href: item.href!,
|
|
125
|
+
startWith: item.activeStartsWith,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const handleItemClick = (e: React.MouseEvent) => {
|
|
129
|
+
if (hasChildren) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
onToggle(item.id);
|
|
132
|
+
}
|
|
133
|
+
// anchorProps.onClick handles navigation automatically
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Box className="alepha-sidebar-item-wrapper">
|
|
138
|
+
<UnstyledButton
|
|
139
|
+
component="a"
|
|
140
|
+
{...anchorProps}
|
|
141
|
+
className={`alepha-sidebar-item alepha-sidebar-level-${level} ${isActive ? "alepha-sidebar-item-active" : ""}`}
|
|
142
|
+
onClick={hasChildren ? handleItemClick : anchorProps.onClick}
|
|
143
|
+
>
|
|
144
|
+
<Flex justify="space-between" align="center" w="100%">
|
|
145
|
+
<Flex className="alepha-sidebar-item-content" align="center" gap={10}>
|
|
146
|
+
<Box className="alepha-sidebar-item-icon">
|
|
147
|
+
{item.icon || <IconCircle size={16} />}
|
|
148
|
+
</Box>
|
|
149
|
+
<Box className="alepha-sidebar-item-label">{item.label}</Box>
|
|
150
|
+
</Flex>
|
|
151
|
+
{hasChildren && (
|
|
152
|
+
<Box className="alepha-sidebar-item-caret">
|
|
153
|
+
{isOpen ? (
|
|
154
|
+
<IconChevronDown size={14} />
|
|
155
|
+
) : (
|
|
156
|
+
<IconChevronRight size={14} />
|
|
157
|
+
)}
|
|
158
|
+
</Box>
|
|
159
|
+
)}
|
|
160
|
+
</Flex>
|
|
161
|
+
</UnstyledButton>
|
|
162
|
+
{hasChildren && isOpen && (
|
|
163
|
+
<Box className="alepha-sidebar-children" data-parent-level={level}>
|
|
164
|
+
{(level === 0 || level === 1) && (
|
|
165
|
+
<Box className="alepha-sidebar-vertical-bar" />
|
|
166
|
+
)}
|
|
167
|
+
<Box className="alepha-sidebar-children-items">
|
|
168
|
+
{item.children!.map((child) => (
|
|
169
|
+
<SidebarItem
|
|
170
|
+
key={child.id}
|
|
171
|
+
item={child}
|
|
172
|
+
level={level + 1}
|
|
173
|
+
openIds={openIds}
|
|
174
|
+
onToggle={onToggle}
|
|
175
|
+
onItemClick={onItemClick}
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
178
|
+
</Box>
|
|
179
|
+
</Box>
|
|
180
|
+
)}
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
186
|
+
// SidebarItemButton - Component for items without href (buttons with onClick)
|
|
187
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
const SidebarItemButton: React.FC<SidebarItemProps> = ({
|
|
190
|
+
item,
|
|
191
|
+
level,
|
|
192
|
+
openIds,
|
|
193
|
+
onToggle,
|
|
194
|
+
onItemClick,
|
|
195
|
+
}) => {
|
|
196
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
197
|
+
const isOpen = openIds.has(item.id);
|
|
198
|
+
|
|
199
|
+
const handleItemClick = (e: React.MouseEvent) => {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
if (hasChildren) {
|
|
202
|
+
onToggle(item.id);
|
|
203
|
+
} else {
|
|
204
|
+
onItemClick?.(item);
|
|
205
|
+
item.onClick?.();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Box className="alepha-sidebar-item-wrapper">
|
|
211
|
+
<UnstyledButton
|
|
212
|
+
component="button"
|
|
213
|
+
className={`alepha-sidebar-item alepha-sidebar-level-${level}`}
|
|
214
|
+
onClick={handleItemClick}
|
|
215
|
+
>
|
|
216
|
+
<Flex justify="space-between" align="center" w="100%">
|
|
217
|
+
<Flex className="alepha-sidebar-item-content" align="center" gap={10}>
|
|
218
|
+
<Box className="alepha-sidebar-item-icon">
|
|
219
|
+
{item.icon || <IconCircle size={16} />}
|
|
220
|
+
</Box>
|
|
221
|
+
<Box className="alepha-sidebar-item-label">{item.label}</Box>
|
|
222
|
+
</Flex>
|
|
223
|
+
{hasChildren && (
|
|
224
|
+
<Box className="alepha-sidebar-item-caret">
|
|
225
|
+
{isOpen ? (
|
|
226
|
+
<IconChevronDown size={14} />
|
|
227
|
+
) : (
|
|
228
|
+
<IconChevronRight size={14} />
|
|
229
|
+
)}
|
|
230
|
+
</Box>
|
|
231
|
+
)}
|
|
232
|
+
</Flex>
|
|
233
|
+
</UnstyledButton>
|
|
234
|
+
{hasChildren && isOpen && (
|
|
235
|
+
<Box className="alepha-sidebar-children" data-parent-level={level}>
|
|
236
|
+
{(level === 0 || level === 1) && (
|
|
237
|
+
<Box className="alepha-sidebar-vertical-bar" />
|
|
238
|
+
)}
|
|
239
|
+
<Box className="alepha-sidebar-children-items">
|
|
240
|
+
{item.children!.map((child) => (
|
|
241
|
+
<SidebarItem
|
|
242
|
+
key={child.id}
|
|
243
|
+
item={child}
|
|
244
|
+
level={level + 1}
|
|
245
|
+
openIds={openIds}
|
|
246
|
+
onToggle={onToggle}
|
|
247
|
+
onItemClick={onItemClick}
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</Box>
|
|
251
|
+
</Box>
|
|
252
|
+
)}
|
|
253
|
+
</Box>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TObject } from "@alepha/core";
|
|
2
2
|
import type { FormModel } from "@alepha/react-form";
|
|
3
|
-
import {
|
|
3
|
+
import { Flex, Grid } from "@mantine/core";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import Action, { type ActionSubmitProps } from "./Action";
|
|
6
6
|
import Control, { type ControlProps } from "./Control";
|
|
@@ -22,6 +22,7 @@ export interface TypeFormProps<T extends TObject> {
|
|
|
22
22
|
skipFormElement?: boolean;
|
|
23
23
|
skipSubmitButton?: boolean;
|
|
24
24
|
submitButtonProps?: Partial<Omit<ActionSubmitProps, "form">>;
|
|
25
|
+
resetButtonProps?: Partial<Omit<ActionSubmitProps, "form">>;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -52,7 +53,7 @@ export interface TypeFormProps<T extends TObject> {
|
|
|
52
53
|
const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
|
|
53
54
|
const {
|
|
54
55
|
form,
|
|
55
|
-
columns =
|
|
56
|
+
columns = 3,
|
|
56
57
|
children,
|
|
57
58
|
controlProps,
|
|
58
59
|
skipFormElement = false,
|
|
@@ -97,7 +98,7 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
|
|
|
97
98
|
? {
|
|
98
99
|
xs: 12,
|
|
99
100
|
sm: 6,
|
|
100
|
-
lg: 12 /
|
|
101
|
+
lg: 12 / columns,
|
|
101
102
|
}
|
|
102
103
|
: {
|
|
103
104
|
base: columns.base ? 12 / columns.base : undefined,
|
|
@@ -134,25 +135,24 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
|
|
|
134
135
|
};
|
|
135
136
|
|
|
136
137
|
const content = (
|
|
137
|
-
<
|
|
138
|
+
<Flex direction={"column"} gap={"sm"}>
|
|
138
139
|
{renderFields()}
|
|
139
140
|
{!skipSubmitButton && (
|
|
140
|
-
<
|
|
141
|
-
{submitButtonProps
|
|
142
|
-
|
|
141
|
+
<Flex>
|
|
142
|
+
<Action form={form} {...submitButtonProps}>
|
|
143
|
+
{submitButtonProps?.children ?? "Submit"}
|
|
144
|
+
</Action>
|
|
145
|
+
<button type={"reset"}>Reset</button>
|
|
146
|
+
</Flex>
|
|
143
147
|
)}
|
|
144
|
-
</
|
|
148
|
+
</Flex>
|
|
145
149
|
);
|
|
146
150
|
|
|
147
151
|
if (skipFormElement) {
|
|
148
152
|
return content;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
return
|
|
152
|
-
<form onSubmit={form.onSubmit} noValidate>
|
|
153
|
-
{content}
|
|
154
|
-
</form>
|
|
155
|
-
);
|
|
155
|
+
return <form {...form.props}>{content}</form>;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
export default TypeForm;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Button, Group, Text } from "@mantine/core";
|
|
2
|
+
import type { AlertDialogProps } from "../../services/DialogService";
|
|
3
|
+
|
|
4
|
+
export function AlertDialog({ options, onClose }: AlertDialogProps) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{options?.message && <Text mb="md">{options.message}</Text>}
|
|
8
|
+
<Group justify="flex-end">
|
|
9
|
+
<Button onClick={onClose}>{options?.okLabel || "OK"}</Button>
|
|
10
|
+
</Group>
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Button, Group, Text } from "@mantine/core";
|
|
2
|
+
import type { ConfirmDialogProps } from "../../services/DialogService";
|
|
3
|
+
|
|
4
|
+
export function ConfirmDialog({ options, onConfirm }: ConfirmDialogProps) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{options?.message && <Text mb="md">{options.message}</Text>}
|
|
8
|
+
<Group justify="flex-end">
|
|
9
|
+
<Button variant="subtle" onClick={() => onConfirm(false)}>
|
|
10
|
+
{options?.cancelLabel || "Cancel"}
|
|
11
|
+
</Button>
|
|
12
|
+
<Button
|
|
13
|
+
color={options?.confirmColor || "blue"}
|
|
14
|
+
onClick={() => onConfirm(true)}
|
|
15
|
+
>
|
|
16
|
+
{options?.confirmLabel || "Confirm"}
|
|
17
|
+
</Button>
|
|
18
|
+
</Group>
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Button, Group, Text, TextInput } from "@mantine/core";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import type { PromptDialogProps } from "../../services/DialogService";
|
|
4
|
+
|
|
5
|
+
export function PromptDialog({ options, onSubmit }: PromptDialogProps) {
|
|
6
|
+
const [value, setValue] = useState(options?.defaultValue || "");
|
|
7
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
// Auto-focus the input when the dialog opens
|
|
11
|
+
inputRef.current?.focus();
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
const handleSubmit = () => {
|
|
15
|
+
if (!options?.required || value.trim()) {
|
|
16
|
+
onSubmit(value);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
21
|
+
if (event.key === "Enter") {
|
|
22
|
+
handleSubmit();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{options?.message && <Text mb="md">{options.message}</Text>}
|
|
29
|
+
<TextInput
|
|
30
|
+
ref={inputRef}
|
|
31
|
+
label={options?.label}
|
|
32
|
+
placeholder={options?.placeholder}
|
|
33
|
+
value={value}
|
|
34
|
+
onChange={(event) => setValue(event.currentTarget.value)}
|
|
35
|
+
onKeyDown={handleKeyDown}
|
|
36
|
+
required={options?.required}
|
|
37
|
+
mb="md"
|
|
38
|
+
/>
|
|
39
|
+
<Group justify="flex-end">
|
|
40
|
+
<Button variant="subtle" onClick={() => onSubmit(null)}>
|
|
41
|
+
{options?.cancelLabel || "Cancel"}
|
|
42
|
+
</Button>
|
|
43
|
+
<Button
|
|
44
|
+
onClick={handleSubmit}
|
|
45
|
+
disabled={options?.required && !value.trim()}
|
|
46
|
+
>
|
|
47
|
+
{options?.submitLabel || "OK"}
|
|
48
|
+
</Button>
|
|
49
|
+
</Group>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useInject } from "@alepha/react";
|
|
2
|
+
import { DialogService } from "../services/DialogService.tsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Use this hook to access the Dialog Service for showing various dialog types.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const dialog = useDialog();
|
|
9
|
+
* await dialog.alert({ title: "Alert", message: "This is an alert message" });
|
|
10
|
+
* const confirmed = await dialog.confirm({ title: "Confirm", message: "Are you sure?" });
|
|
11
|
+
* const input = await dialog.prompt({ title: "Input", message: "Enter your name:" });
|
|
12
|
+
*/
|
|
13
|
+
export const useDialog = (): DialogService => {
|
|
14
|
+
return useInject(DialogService);
|
|
15
|
+
};
|