@alepha/ui 0.10.6 → 0.11.0
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-DDbIijPF.js +96 -0
- package/dist/AlephaMantineProvider-DDbIijPF.js.map +1 -0
- package/dist/AlephaMantineProvider-pOu8hOzK.js +3 -0
- package/dist/index.d.ts +560 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1341 -109
- package/dist/index.js.map +1 -1
- package/package.json +21 -12
- package/src/RootRouter.ts +8 -0
- package/src/components/Action.tsx +296 -135
- package/src/components/AlephaMantineProvider.tsx +34 -31
- package/src/components/Control.tsx +251 -236
- package/src/components/ControlDate.tsx +104 -0
- package/src/components/ControlSelect.tsx +196 -0
- package/src/components/DarkModeButton.tsx +82 -0
- package/src/components/DataTable.css +199 -0
- package/src/components/DataTable.tsx +724 -0
- package/src/components/Omnibar.tsx +77 -0
- package/src/components/Sidebar.css +217 -0
- package/src/components/Sidebar.tsx +255 -0
- package/src/components/TypeForm.tsx +158 -0
- 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/hooks/useToast.ts +14 -0
- package/src/index.ts +54 -10
- package/src/services/DialogService.tsx +207 -0
- package/src/services/ToastService.tsx +71 -0
- package/src/utils/icons.tsx +121 -0
- package/src/utils/parseInput.ts +125 -0
- package/src/utils/string.ts +21 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useFormState } from "@alepha/react-form";
|
|
2
|
+
import {
|
|
3
|
+
Autocomplete,
|
|
4
|
+
type AutocompleteProps,
|
|
5
|
+
Flex,
|
|
6
|
+
Input,
|
|
7
|
+
MultiSelect,
|
|
8
|
+
type MultiSelectProps,
|
|
9
|
+
SegmentedControl,
|
|
10
|
+
type SegmentedControlProps,
|
|
11
|
+
Select,
|
|
12
|
+
type SelectProps,
|
|
13
|
+
TagsInput,
|
|
14
|
+
type TagsInputProps,
|
|
15
|
+
} from "@mantine/core";
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
import { type GenericControlProps, parseInput } from "../utils/parseInput.ts";
|
|
18
|
+
|
|
19
|
+
export type SelectValueLabel =
|
|
20
|
+
| string
|
|
21
|
+
| { value: string; label: string; icon?: string };
|
|
22
|
+
|
|
23
|
+
export interface ControlSelectProps extends GenericControlProps {
|
|
24
|
+
select?: boolean | SelectProps;
|
|
25
|
+
multi?: boolean | MultiSelectProps;
|
|
26
|
+
tags?: boolean | TagsInputProps;
|
|
27
|
+
autocomplete?: boolean | AutocompleteProps;
|
|
28
|
+
segmented?: boolean | Partial<SegmentedControlProps>;
|
|
29
|
+
|
|
30
|
+
loader?: () => Promise<SelectValueLabel[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ControlSelect component for handling Select, MultiSelect, and TagsInput.
|
|
35
|
+
*
|
|
36
|
+
* Features:
|
|
37
|
+
* - Basic Select with enum support
|
|
38
|
+
* - MultiSelect for array of enums
|
|
39
|
+
* - TagsInput for array of strings (no enum)
|
|
40
|
+
* - Future: Lazy loading
|
|
41
|
+
* - Future: Searchable/filterable options
|
|
42
|
+
* - Future: Custom option rendering
|
|
43
|
+
*
|
|
44
|
+
* Automatically detects enum values and array types from schema.
|
|
45
|
+
*/
|
|
46
|
+
const ControlSelect = (props: ControlSelectProps) => {
|
|
47
|
+
const form = useFormState(props.input);
|
|
48
|
+
const { inputProps, id, icon } = parseInput(props, form);
|
|
49
|
+
|
|
50
|
+
// Detect if schema is an array type
|
|
51
|
+
const isArray =
|
|
52
|
+
props.input.schema &&
|
|
53
|
+
"type" in props.input.schema &&
|
|
54
|
+
props.input.schema.type === "array";
|
|
55
|
+
|
|
56
|
+
// For arrays, check if items have enum (MultiSelect) or not (TagsInput)
|
|
57
|
+
let itemsEnum: string[] | undefined;
|
|
58
|
+
if (isArray && "items" in props.input.schema && props.input.schema.items) {
|
|
59
|
+
const items: any = props.input.schema.items;
|
|
60
|
+
if ("enum" in items && Array.isArray(items.enum)) {
|
|
61
|
+
itemsEnum = items.enum;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract enum values from schema (for non-array select)
|
|
66
|
+
const enumValues =
|
|
67
|
+
props.input.schema &&
|
|
68
|
+
"enum" in props.input.schema &&
|
|
69
|
+
Array.isArray(props.input.schema.enum)
|
|
70
|
+
? props.input.schema.enum
|
|
71
|
+
: [];
|
|
72
|
+
|
|
73
|
+
const [data, setData] = useState<SelectValueLabel[]>([]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!props.input?.props) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (props.loader) {
|
|
81
|
+
props.loader().then(setData);
|
|
82
|
+
} else {
|
|
83
|
+
setData(enumValues);
|
|
84
|
+
}
|
|
85
|
+
}, [props.input, props.loader]);
|
|
86
|
+
|
|
87
|
+
if (!props.input?.props) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (props.segmented) {
|
|
92
|
+
const segmentedControlProps: Partial<SegmentedControlProps> =
|
|
93
|
+
typeof props.segmented === "object" ? props.segmented : {};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Input.Wrapper {...inputProps}>
|
|
97
|
+
<Flex mt={"calc(var(--mantine-spacing-xs) / 2)"}>
|
|
98
|
+
<SegmentedControl
|
|
99
|
+
disabled={inputProps.disabled}
|
|
100
|
+
defaultValue={String(props.input.props.defaultValue)}
|
|
101
|
+
{...segmentedControlProps}
|
|
102
|
+
onChange={(value) => {
|
|
103
|
+
props.input.set(value);
|
|
104
|
+
}}
|
|
105
|
+
data={data.slice(0, 10)}
|
|
106
|
+
/>
|
|
107
|
+
</Flex>
|
|
108
|
+
</Input.Wrapper>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (props.autocomplete) {
|
|
113
|
+
const autocompleteProps =
|
|
114
|
+
typeof props.autocomplete === "object" ? props.autocomplete : {};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Autocomplete
|
|
118
|
+
{...inputProps}
|
|
119
|
+
id={id}
|
|
120
|
+
leftSection={icon}
|
|
121
|
+
data={data}
|
|
122
|
+
{...props.input.props}
|
|
123
|
+
{...autocompleteProps}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// region <TagsInput/> - for array of strings without enum
|
|
129
|
+
if ((isArray && !itemsEnum) || props.tags) {
|
|
130
|
+
const tagsInputProps = typeof props.tags === "object" ? props.tags : {};
|
|
131
|
+
return (
|
|
132
|
+
<TagsInput
|
|
133
|
+
{...inputProps}
|
|
134
|
+
id={id}
|
|
135
|
+
leftSection={icon}
|
|
136
|
+
defaultValue={
|
|
137
|
+
Array.isArray(props.input.props.defaultValue)
|
|
138
|
+
? props.input.props.defaultValue
|
|
139
|
+
: []
|
|
140
|
+
}
|
|
141
|
+
onChange={(value) => {
|
|
142
|
+
props.input.set(value);
|
|
143
|
+
}}
|
|
144
|
+
{...tagsInputProps}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
// endregion
|
|
149
|
+
|
|
150
|
+
// region <MultiSelect/> - for array of enums
|
|
151
|
+
if ((isArray && itemsEnum) || props.multi) {
|
|
152
|
+
const data =
|
|
153
|
+
itemsEnum?.map((value: string) => ({
|
|
154
|
+
value,
|
|
155
|
+
label: value,
|
|
156
|
+
})) || [];
|
|
157
|
+
|
|
158
|
+
const multiSelectProps = typeof props.multi === "object" ? props.multi : {};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<MultiSelect
|
|
162
|
+
{...inputProps}
|
|
163
|
+
id={id}
|
|
164
|
+
leftSection={icon}
|
|
165
|
+
data={data}
|
|
166
|
+
defaultValue={
|
|
167
|
+
Array.isArray(props.input.props.defaultValue)
|
|
168
|
+
? props.input.props.defaultValue
|
|
169
|
+
: []
|
|
170
|
+
}
|
|
171
|
+
onChange={(value) => {
|
|
172
|
+
props.input.set(value);
|
|
173
|
+
}}
|
|
174
|
+
{...multiSelectProps}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
// endregion
|
|
179
|
+
|
|
180
|
+
// region <Select/> - for single enum value
|
|
181
|
+
const selectProps = typeof props.select === "object" ? props.select : {};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Select
|
|
185
|
+
{...inputProps}
|
|
186
|
+
id={id}
|
|
187
|
+
leftSection={icon}
|
|
188
|
+
data={data}
|
|
189
|
+
{...props.input.props}
|
|
190
|
+
{...selectProps}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
// endregion
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default ControlSelect;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Flex,
|
|
4
|
+
SegmentedControl,
|
|
5
|
+
useComputedColorScheme,
|
|
6
|
+
useMantineColorScheme,
|
|
7
|
+
} from "@mantine/core";
|
|
8
|
+
import { IconMoon, IconSun } from "@tabler/icons-react";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
|
|
11
|
+
export interface DarkModeButtonProps {
|
|
12
|
+
mode?: "minimal" | "segmented";
|
|
13
|
+
size?: string | number;
|
|
14
|
+
variant?:
|
|
15
|
+
| "filled"
|
|
16
|
+
| "light"
|
|
17
|
+
| "outline"
|
|
18
|
+
| "default"
|
|
19
|
+
| "subtle"
|
|
20
|
+
| "transparent";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DarkModeButton = (props: DarkModeButtonProps) => {
|
|
24
|
+
const { setColorScheme } = useMantineColorScheme();
|
|
25
|
+
const computedColorScheme = useComputedColorScheme("light");
|
|
26
|
+
const [colorScheme, setColorScheme2] = useState("default");
|
|
27
|
+
const mode = props.mode ?? "minimal";
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setColorScheme2(computedColorScheme);
|
|
31
|
+
}, [computedColorScheme]);
|
|
32
|
+
|
|
33
|
+
const toggleColorScheme = () => {
|
|
34
|
+
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (mode === "segmented") {
|
|
38
|
+
return (
|
|
39
|
+
<SegmentedControl
|
|
40
|
+
value={colorScheme}
|
|
41
|
+
onChange={(value) => setColorScheme(value as "light" | "dark")}
|
|
42
|
+
data={[
|
|
43
|
+
{
|
|
44
|
+
value: "light",
|
|
45
|
+
label: (
|
|
46
|
+
<Flex h={20} align="center" justify="center">
|
|
47
|
+
<IconSun size={16} />
|
|
48
|
+
</Flex>
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: "dark",
|
|
53
|
+
label: (
|
|
54
|
+
<Flex h={20} align="center" justify="center">
|
|
55
|
+
<IconMoon size={16} />
|
|
56
|
+
</Flex>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ActionIcon
|
|
66
|
+
onClick={toggleColorScheme}
|
|
67
|
+
variant={props.variant ?? "default"}
|
|
68
|
+
size={props.size ?? "lg"}
|
|
69
|
+
aria-label="Toggle color scheme"
|
|
70
|
+
>
|
|
71
|
+
{colorScheme === "dark" ? (
|
|
72
|
+
<IconSun size={20} />
|
|
73
|
+
) : colorScheme === "light" ? (
|
|
74
|
+
<IconMoon size={20} />
|
|
75
|
+
) : (
|
|
76
|
+
<Flex h={20} />
|
|
77
|
+
)}
|
|
78
|
+
</ActionIcon>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default DarkModeButton;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* ------------------------------------------------------------------------------------------------------------------ */
|
|
2
|
+
/* DataTable Component Styles */
|
|
3
|
+
/* ------------------------------------------------------------------------------------------------------------------ */
|
|
4
|
+
|
|
5
|
+
.alepha-datatable-container {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.alepha-datatable-toolbar {
|
|
12
|
+
border-radius: var(--mantine-radius-md);
|
|
13
|
+
background: var(--mantine-color-body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.alepha-datatable-search-input {
|
|
17
|
+
min-width: 200px;
|
|
18
|
+
max-width: 300px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.alepha-datatable-page-size-select {
|
|
22
|
+
width: 120px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.alepha-datatable-table {
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.alepha-datatable-th {
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
user-select: none;
|
|
33
|
+
transition: background-color 0.2s ease;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.alepha-datatable-th:hover {
|
|
37
|
+
background-color: var(--mantine-color-gray-0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[data-mantine-color-scheme="dark"] .alepha-datatable-th:hover {
|
|
41
|
+
background-color: var(--mantine-color-dark-6);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.alepha-datatable-tr {
|
|
45
|
+
transition: background-color 0.15s ease;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.alepha-datatable-tr.alepha-datatable-selected {
|
|
49
|
+
background-color: var(--mantine-color-blue-0) !important;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-mantine-color-scheme="dark"]
|
|
53
|
+
.alepha-datatable-tr.alepha-datatable-selected {
|
|
54
|
+
background-color: rgba(34, 139, 230, 0.1) !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.alepha-datatable-checkbox-column {
|
|
58
|
+
width: 40px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.alepha-datatable-actions-column {
|
|
63
|
+
width: 100px;
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.alepha-datatable-sort-icon {
|
|
68
|
+
color: var(--mantine-color-blue-6);
|
|
69
|
+
transition: transform 0.2s ease;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.alepha-datatable-sort-icon-inactive {
|
|
73
|
+
color: var(--mantine-color-gray-5);
|
|
74
|
+
opacity: 0;
|
|
75
|
+
transition: opacity 0.2s ease;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.alepha-datatable-th:hover .alepha-datatable-sort-icon-inactive {
|
|
79
|
+
opacity: 0.5;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Loading state */
|
|
83
|
+
.alepha-datatable-loading {
|
|
84
|
+
position: relative;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
opacity: 0.5;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.alepha-datatable-loading-overlay {
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: 0;
|
|
92
|
+
left: 0;
|
|
93
|
+
right: 0;
|
|
94
|
+
bottom: 0;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
background: rgba(255, 255, 255, 0.8);
|
|
99
|
+
z-index: 10;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-mantine-color-scheme="dark"] .alepha-datatable-loading-overlay {
|
|
103
|
+
background: rgba(26, 27, 30, 0.8);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Sticky header styles */
|
|
107
|
+
.alepha-datatable-sticky-header {
|
|
108
|
+
position: sticky;
|
|
109
|
+
top: 0;
|
|
110
|
+
z-index: 5;
|
|
111
|
+
background: var(--mantine-color-body);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Zebra striping adjustments */
|
|
115
|
+
.alepha-datatable-striped tbody tr:nth-of-type(odd) {
|
|
116
|
+
background-color: var(--mantine-color-gray-0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[data-mantine-color-scheme="dark"]
|
|
120
|
+
.alepha-datatable-striped
|
|
121
|
+
tbody
|
|
122
|
+
tr:nth-of-type(odd) {
|
|
123
|
+
background-color: var(--mantine-color-dark-7);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Empty state */
|
|
127
|
+
.alepha-datatable-empty-state {
|
|
128
|
+
padding: 3rem 1rem;
|
|
129
|
+
text-align: center;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.alepha-datatable-empty-icon {
|
|
133
|
+
color: var(--mantine-color-gray-5);
|
|
134
|
+
margin-bottom: 1rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Footer */
|
|
138
|
+
.alepha-datatable-footer {
|
|
139
|
+
border-top: 1px solid var(--mantine-color-gray-2);
|
|
140
|
+
padding-top: var(--mantine-spacing-sm);
|
|
141
|
+
margin-top: var(--mantine-spacing-xs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-mantine-color-scheme="dark"] .alepha-datatable-footer {
|
|
145
|
+
border-top-color: var(--mantine-color-dark-5);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Custom scrollbar for better UX */
|
|
149
|
+
.alepha-datatable-container ::-webkit-scrollbar {
|
|
150
|
+
width: 8px;
|
|
151
|
+
height: 8px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.alepha-datatable-container ::-webkit-scrollbar-track {
|
|
155
|
+
background: var(--mantine-color-gray-1);
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
[data-mantine-color-scheme="dark"]
|
|
160
|
+
.alepha-datatable-container
|
|
161
|
+
::-webkit-scrollbar-track {
|
|
162
|
+
background: var(--mantine-color-dark-6);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.alepha-datatable-container ::-webkit-scrollbar-thumb {
|
|
166
|
+
background: var(--mantine-color-gray-4);
|
|
167
|
+
border-radius: 4px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
[data-mantine-color-scheme="dark"]
|
|
171
|
+
.alepha-datatable-container
|
|
172
|
+
::-webkit-scrollbar-thumb {
|
|
173
|
+
background: var(--mantine-color-dark-4);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.alepha-datatable-container ::-webkit-scrollbar-thumb:hover {
|
|
177
|
+
background: var(--mantine-color-gray-5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[data-mantine-color-scheme="dark"]
|
|
181
|
+
.alepha-datatable-container
|
|
182
|
+
::-webkit-scrollbar-thumb:hover {
|
|
183
|
+
background: var(--mantine-color-dark-3);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Responsive */
|
|
187
|
+
@media (max-width: 768px) {
|
|
188
|
+
.alepha-datatable-toolbar {
|
|
189
|
+
padding: var(--mantine-spacing-sm);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.alepha-datatable-search-input {
|
|
193
|
+
min-width: 150px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.alepha-datatable-page-size-select {
|
|
197
|
+
width: 100px;
|
|
198
|
+
}
|
|
199
|
+
}
|