@alepha/ui 0.10.5 → 0.10.7
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-EemOtraW.js +3 -0
- package/dist/AlephaMantineProvider-WfiC2EH6.js +95 -0
- package/dist/AlephaMantineProvider-WfiC2EH6.js.map +1 -0
- package/dist/index.d.ts +236 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +520 -81
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/src/RootRouter.ts +8 -0
- package/src/components/Action.tsx +134 -134
- package/src/components/AlephaMantineProvider.tsx +34 -31
- package/src/components/Control.tsx +383 -229
- package/src/components/ControlDate.tsx +112 -0
- package/src/components/ControlSelect.tsx +134 -0
- package/src/components/DarkModeButton.tsx +75 -0
- package/src/components/Omnibar.tsx +76 -0
- package/src/components/TypeForm.tsx +158 -0
- package/src/hooks/useToast.ts +14 -0
- package/src/index.ts +25 -9
- package/src/services/ToastService.tsx +71 -0
- package/src/utils/icons.tsx +121 -0
- package/src/utils/string.ts +21 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useFormState } from "@alepha/react-form";
|
|
2
|
+
import {
|
|
3
|
+
DateInput,
|
|
4
|
+
type DateInputProps,
|
|
5
|
+
DateTimePicker,
|
|
6
|
+
type DateTimePickerProps,
|
|
7
|
+
TimeInput,
|
|
8
|
+
type TimeInputProps,
|
|
9
|
+
} from "@mantine/dates";
|
|
10
|
+
import { type GenericControlProps, parseInput } from "./Control.tsx";
|
|
11
|
+
|
|
12
|
+
export interface ControlDateProps extends GenericControlProps {
|
|
13
|
+
date?: boolean | DateInputProps;
|
|
14
|
+
datetime?: boolean | DateTimePickerProps;
|
|
15
|
+
time?: boolean | TimeInputProps;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ControlDate component for handling date, datetime, and time inputs.
|
|
20
|
+
*
|
|
21
|
+
* Features:
|
|
22
|
+
* - DateInput for date format
|
|
23
|
+
* - DateTimePicker for date-time format
|
|
24
|
+
* - TimeInput for time format
|
|
25
|
+
*
|
|
26
|
+
* Automatically detects date formats from schema and renders appropriate picker.
|
|
27
|
+
*/
|
|
28
|
+
const ControlDate = (props: ControlDateProps) => {
|
|
29
|
+
const form = useFormState(props.input);
|
|
30
|
+
const { inputProps, id, icon } = parseInput(props, form);
|
|
31
|
+
if (!props.input?.props) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Detect format from schema
|
|
36
|
+
const format =
|
|
37
|
+
props.input.schema &&
|
|
38
|
+
"format" in props.input.schema &&
|
|
39
|
+
typeof props.input.schema.format === "string"
|
|
40
|
+
? props.input.schema.format
|
|
41
|
+
: undefined;
|
|
42
|
+
|
|
43
|
+
// region <DateTimePicker/>
|
|
44
|
+
if (props.datetime || format === "date-time") {
|
|
45
|
+
const dateTimePickerProps =
|
|
46
|
+
typeof props.datetime === "object" ? props.datetime : {};
|
|
47
|
+
return (
|
|
48
|
+
<DateTimePicker
|
|
49
|
+
{...inputProps}
|
|
50
|
+
id={id}
|
|
51
|
+
leftSection={icon}
|
|
52
|
+
defaultValue={
|
|
53
|
+
props.input.props.defaultValue
|
|
54
|
+
? new Date(props.input.props.defaultValue)
|
|
55
|
+
: undefined
|
|
56
|
+
}
|
|
57
|
+
onChange={(value) => {
|
|
58
|
+
props.input.set(value ? new Date(value).toISOString() : undefined);
|
|
59
|
+
}}
|
|
60
|
+
{...dateTimePickerProps}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
//endregion
|
|
65
|
+
|
|
66
|
+
// region <DateInput/>
|
|
67
|
+
if (props.date || format === "date") {
|
|
68
|
+
const dateInputProps = typeof props.date === "object" ? props.date : {};
|
|
69
|
+
return (
|
|
70
|
+
<DateInput
|
|
71
|
+
{...inputProps}
|
|
72
|
+
id={id}
|
|
73
|
+
leftSection={icon}
|
|
74
|
+
defaultValue={
|
|
75
|
+
props.input.props.defaultValue
|
|
76
|
+
? new Date(props.input.props.defaultValue)
|
|
77
|
+
: undefined
|
|
78
|
+
}
|
|
79
|
+
onChange={(value) => {
|
|
80
|
+
props.input.set(
|
|
81
|
+
value ? new Date(value).toISOString().slice(0, 10) : undefined,
|
|
82
|
+
);
|
|
83
|
+
}}
|
|
84
|
+
{...dateInputProps}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
//endregion
|
|
89
|
+
|
|
90
|
+
// region <TimeInput/>
|
|
91
|
+
if (props.time || format === "time") {
|
|
92
|
+
const timeInputProps = typeof props.time === "object" ? props.time : {};
|
|
93
|
+
return (
|
|
94
|
+
<TimeInput
|
|
95
|
+
{...inputProps}
|
|
96
|
+
id={id}
|
|
97
|
+
leftSection={icon}
|
|
98
|
+
defaultValue={props.input.props.defaultValue}
|
|
99
|
+
onChange={(event) => {
|
|
100
|
+
props.input.set(event.currentTarget.value);
|
|
101
|
+
}}
|
|
102
|
+
{...timeInputProps}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
//endregion
|
|
107
|
+
|
|
108
|
+
// Fallback - shouldn't happen
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default ControlDate;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useFormState } from "@alepha/react-form";
|
|
2
|
+
import {
|
|
3
|
+
MultiSelect,
|
|
4
|
+
type MultiSelectProps,
|
|
5
|
+
Select,
|
|
6
|
+
type SelectProps,
|
|
7
|
+
TagsInput,
|
|
8
|
+
type TagsInputProps,
|
|
9
|
+
} from "@mantine/core";
|
|
10
|
+
import { type GenericControlProps, parseInput } from "./Control.tsx";
|
|
11
|
+
|
|
12
|
+
export interface ControlSelectProps extends GenericControlProps {
|
|
13
|
+
select?: boolean | SelectProps;
|
|
14
|
+
multi?: boolean | MultiSelectProps;
|
|
15
|
+
tags?: boolean | TagsInputProps;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ControlSelect component for handling Select, MultiSelect, and TagsInput.
|
|
20
|
+
*
|
|
21
|
+
* Features:
|
|
22
|
+
* - Basic Select with enum support
|
|
23
|
+
* - MultiSelect for array of enums
|
|
24
|
+
* - TagsInput for array of strings (no enum)
|
|
25
|
+
* - Future: Lazy loading
|
|
26
|
+
* - Future: Searchable/filterable options
|
|
27
|
+
* - Future: Custom option rendering
|
|
28
|
+
*
|
|
29
|
+
* Automatically detects enum values and array types from schema.
|
|
30
|
+
*/
|
|
31
|
+
const ControlSelect = (props: ControlSelectProps) => {
|
|
32
|
+
const form = useFormState(props.input);
|
|
33
|
+
const { inputProps, id, icon } = parseInput(props, form);
|
|
34
|
+
if (!props.input?.props) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Detect if schema is an array type
|
|
39
|
+
const isArray =
|
|
40
|
+
props.input.schema &&
|
|
41
|
+
"type" in props.input.schema &&
|
|
42
|
+
props.input.schema.type === "array";
|
|
43
|
+
|
|
44
|
+
// For arrays, check if items have enum (MultiSelect) or not (TagsInput)
|
|
45
|
+
let itemsEnum: string[] | undefined;
|
|
46
|
+
if (isArray && "items" in props.input.schema && props.input.schema.items) {
|
|
47
|
+
const items: any = props.input.schema.items;
|
|
48
|
+
if ("enum" in items && Array.isArray(items.enum)) {
|
|
49
|
+
itemsEnum = items.enum;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Extract enum values from schema (for non-array select)
|
|
54
|
+
const enumValues =
|
|
55
|
+
props.input.schema &&
|
|
56
|
+
"enum" in props.input.schema &&
|
|
57
|
+
Array.isArray(props.input.schema.enum)
|
|
58
|
+
? props.input.schema.enum
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
// region <TagsInput/> - for array of strings without enum
|
|
62
|
+
if ((isArray && !itemsEnum) || props.tags) {
|
|
63
|
+
const tagsInputProps = typeof props.tags === "object" ? props.tags : {};
|
|
64
|
+
return (
|
|
65
|
+
<TagsInput
|
|
66
|
+
{...inputProps}
|
|
67
|
+
id={id}
|
|
68
|
+
leftSection={icon}
|
|
69
|
+
defaultValue={
|
|
70
|
+
Array.isArray(props.input.props.defaultValue)
|
|
71
|
+
? props.input.props.defaultValue
|
|
72
|
+
: []
|
|
73
|
+
}
|
|
74
|
+
onChange={(value) => {
|
|
75
|
+
props.input.set(value);
|
|
76
|
+
}}
|
|
77
|
+
{...tagsInputProps}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
// endregion
|
|
82
|
+
|
|
83
|
+
// region <MultiSelect/> - for array of enums
|
|
84
|
+
if ((isArray && itemsEnum) || props.multi) {
|
|
85
|
+
const data =
|
|
86
|
+
itemsEnum?.map((value: string) => ({
|
|
87
|
+
value,
|
|
88
|
+
label: value,
|
|
89
|
+
})) || [];
|
|
90
|
+
|
|
91
|
+
const multiSelectProps = typeof props.multi === "object" ? props.multi : {};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<MultiSelect
|
|
95
|
+
{...inputProps}
|
|
96
|
+
id={id}
|
|
97
|
+
leftSection={icon}
|
|
98
|
+
data={data}
|
|
99
|
+
defaultValue={
|
|
100
|
+
Array.isArray(props.input.props.defaultValue)
|
|
101
|
+
? props.input.props.defaultValue
|
|
102
|
+
: []
|
|
103
|
+
}
|
|
104
|
+
onChange={(value) => {
|
|
105
|
+
props.input.set(value);
|
|
106
|
+
}}
|
|
107
|
+
{...multiSelectProps}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
// endregion
|
|
112
|
+
|
|
113
|
+
// region <Select/> - for single enum value
|
|
114
|
+
const data = enumValues.map((value: string) => ({
|
|
115
|
+
value,
|
|
116
|
+
label: value,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
const selectProps = typeof props.select === "object" ? props.select : {};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Select
|
|
123
|
+
{...inputProps}
|
|
124
|
+
id={id}
|
|
125
|
+
leftSection={icon}
|
|
126
|
+
data={data}
|
|
127
|
+
{...props.input.props}
|
|
128
|
+
{...selectProps}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
// endregion
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default ControlSelect;
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
useEffect(() => {
|
|
28
|
+
setColorScheme2(computedColorScheme);
|
|
29
|
+
}, [computedColorScheme]);
|
|
30
|
+
const mode = props.mode ?? "minimal";
|
|
31
|
+
|
|
32
|
+
const toggleColorScheme = () => {
|
|
33
|
+
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (mode === "segmented") {
|
|
37
|
+
return (
|
|
38
|
+
<SegmentedControl
|
|
39
|
+
value={colorScheme}
|
|
40
|
+
onChange={(value) => setColorScheme(value as "light" | "dark")}
|
|
41
|
+
data={[
|
|
42
|
+
{
|
|
43
|
+
value: "light",
|
|
44
|
+
label: (
|
|
45
|
+
<Flex h={20} align="center" justify="center">
|
|
46
|
+
<IconSun size={16} />
|
|
47
|
+
</Flex>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "dark",
|
|
52
|
+
label: (
|
|
53
|
+
<Flex h={20} align="center" justify="center">
|
|
54
|
+
<IconMoon size={16} />
|
|
55
|
+
</Flex>
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<ActionIcon
|
|
65
|
+
onClick={toggleColorScheme}
|
|
66
|
+
variant={props.variant ?? "default"}
|
|
67
|
+
size={props.size ?? "lg"}
|
|
68
|
+
aria-label="Toggle color scheme"
|
|
69
|
+
>
|
|
70
|
+
{colorScheme === "dark" ? <IconSun size={20} /> : <IconMoon size={20} />}
|
|
71
|
+
</ActionIcon>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default DarkModeButton;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Spotlight, type SpotlightActionData } from "@mantine/spotlight";
|
|
2
|
+
import {
|
|
3
|
+
IconDashboard,
|
|
4
|
+
IconFileText,
|
|
5
|
+
IconHome,
|
|
6
|
+
IconSearch,
|
|
7
|
+
IconSettings,
|
|
8
|
+
IconUser,
|
|
9
|
+
} from "@tabler/icons-react";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
|
|
12
|
+
export interface OmnibarProps {
|
|
13
|
+
actions?: SpotlightActionData[];
|
|
14
|
+
shortcut?: string | string[];
|
|
15
|
+
searchPlaceholder?: string;
|
|
16
|
+
nothingFound?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultActions: SpotlightActionData[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "home",
|
|
22
|
+
label: "Home",
|
|
23
|
+
description: "Go to home page",
|
|
24
|
+
onClick: () => console.log("Home"),
|
|
25
|
+
leftSection: <IconHome size={20} />,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "dashboard",
|
|
29
|
+
label: "Dashboard",
|
|
30
|
+
description: "View your dashboard",
|
|
31
|
+
onClick: () => console.log("Dashboard"),
|
|
32
|
+
leftSection: <IconDashboard size={20} />,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "documents",
|
|
36
|
+
label: "Documents",
|
|
37
|
+
description: "Browse all documents",
|
|
38
|
+
onClick: () => console.log("Documents"),
|
|
39
|
+
leftSection: <IconFileText size={20} />,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "profile",
|
|
43
|
+
label: "Profile",
|
|
44
|
+
description: "View and edit your profile",
|
|
45
|
+
onClick: () => console.log("Profile"),
|
|
46
|
+
leftSection: <IconUser size={20} />,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "settings",
|
|
50
|
+
label: "Settings",
|
|
51
|
+
description: "Manage application settings",
|
|
52
|
+
onClick: () => console.log("Settings"),
|
|
53
|
+
leftSection: <IconSettings size={20} />,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const Omnibar = (props: OmnibarProps) => {
|
|
58
|
+
const actions = props.actions ?? defaultActions;
|
|
59
|
+
const shortcut = props.shortcut ?? "mod+K";
|
|
60
|
+
const searchPlaceholder = props.searchPlaceholder ?? "Search...";
|
|
61
|
+
const nothingFound = props.nothingFound ?? "Nothing found...";
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Spotlight
|
|
65
|
+
actions={actions}
|
|
66
|
+
shortcut={shortcut}
|
|
67
|
+
searchProps={{
|
|
68
|
+
leftSection: <IconSearch size={20} />,
|
|
69
|
+
placeholder: searchPlaceholder,
|
|
70
|
+
}}
|
|
71
|
+
nothingFound={nothingFound}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default Omnibar;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { TObject } from "@alepha/core";
|
|
2
|
+
import type { FormModel } from "@alepha/react-form";
|
|
3
|
+
import { Grid, Stack } from "@mantine/core";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import Action, { type ActionSubmitProps } from "./Action";
|
|
6
|
+
import Control, { type ControlProps } from "./Control";
|
|
7
|
+
|
|
8
|
+
export interface TypeFormProps<T extends TObject> {
|
|
9
|
+
form: FormModel<T>;
|
|
10
|
+
columns?:
|
|
11
|
+
| number
|
|
12
|
+
| {
|
|
13
|
+
base?: number;
|
|
14
|
+
xs?: number;
|
|
15
|
+
sm?: number;
|
|
16
|
+
md?: number;
|
|
17
|
+
lg?: number;
|
|
18
|
+
xl?: number;
|
|
19
|
+
};
|
|
20
|
+
children?: (input: FormModel<T>["input"]) => ReactNode;
|
|
21
|
+
controlProps?: Partial<Omit<ControlProps, "input">>;
|
|
22
|
+
skipFormElement?: boolean;
|
|
23
|
+
skipSubmitButton?: boolean;
|
|
24
|
+
submitButtonProps?: Partial<Omit<ActionSubmitProps, "form">>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* TypeForm component that automatically renders all form inputs based on schema.
|
|
29
|
+
* Uses the Control component to render individual fields and Mantine Grid for responsive layout.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import { t } from "alepha";
|
|
34
|
+
* import { useForm } from "@alepha/react-form";
|
|
35
|
+
* import { TypeForm } from "@alepha/ui";
|
|
36
|
+
*
|
|
37
|
+
* const form = useForm({
|
|
38
|
+
* schema: t.object({
|
|
39
|
+
* username: t.text(),
|
|
40
|
+
* email: t.text(),
|
|
41
|
+
* age: t.integer(),
|
|
42
|
+
* subscribe: t.boolean(),
|
|
43
|
+
* }),
|
|
44
|
+
* handler: (values) => {
|
|
45
|
+
* console.log(values);
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* return <TypeForm form={form} columns={2} />;
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
|
|
53
|
+
const {
|
|
54
|
+
form,
|
|
55
|
+
columns = 1,
|
|
56
|
+
children,
|
|
57
|
+
controlProps,
|
|
58
|
+
skipFormElement = false,
|
|
59
|
+
skipSubmitButton = false,
|
|
60
|
+
submitButtonProps,
|
|
61
|
+
} = props;
|
|
62
|
+
|
|
63
|
+
if (!form.options?.schema?.properties) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fieldNames = Object.keys(form.options.schema.properties);
|
|
68
|
+
|
|
69
|
+
// Filter out unsupported field types (objects only, arrays are now supported)
|
|
70
|
+
const supportedFields = fieldNames.filter((fieldName) => {
|
|
71
|
+
const field = form.input[fieldName as keyof typeof form.input];
|
|
72
|
+
if (!field || typeof field !== "object" || !("schema" in field)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const schema: any = field.schema;
|
|
77
|
+
|
|
78
|
+
// Skip if it's an object (not supported by Control)
|
|
79
|
+
// Arrays are now supported via ControlSelect (MultiSelect/TagsInput)
|
|
80
|
+
if ("type" in schema) {
|
|
81
|
+
if (schema.type === "object") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if it has properties (nested object)
|
|
87
|
+
if ("properties" in schema && schema.properties) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Handle column configuration with defaults: xs=1, sm=2, lg=3
|
|
95
|
+
const colSpan =
|
|
96
|
+
typeof columns === "number"
|
|
97
|
+
? {
|
|
98
|
+
xs: 12,
|
|
99
|
+
sm: 6,
|
|
100
|
+
lg: 12 / 3,
|
|
101
|
+
}
|
|
102
|
+
: {
|
|
103
|
+
base: columns.base ? 12 / columns.base : undefined,
|
|
104
|
+
xs: columns.xs ? 12 / columns.xs : 12,
|
|
105
|
+
sm: columns.sm ? 12 / columns.sm : 6,
|
|
106
|
+
md: columns.md ? 12 / columns.md : undefined,
|
|
107
|
+
lg: columns.lg ? 12 / columns.lg : 4,
|
|
108
|
+
xl: columns.xl ? 12 / columns.xl : undefined,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const renderFields = () => {
|
|
112
|
+
if (children) {
|
|
113
|
+
return <>{children(form.input)}</>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Grid>
|
|
118
|
+
{supportedFields.map((fieldName) => {
|
|
119
|
+
const field = form.input[fieldName as keyof typeof form.input];
|
|
120
|
+
|
|
121
|
+
// Type guard to ensure field has the expected structure
|
|
122
|
+
if (!field || typeof field !== "object" || !("schema" in field)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Grid.Col key={fieldName} span={colSpan}>
|
|
128
|
+
<Control input={field as any} {...controlProps} />
|
|
129
|
+
</Grid.Col>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</Grid>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const content = (
|
|
137
|
+
<Stack>
|
|
138
|
+
{renderFields()}
|
|
139
|
+
{!skipSubmitButton && (
|
|
140
|
+
<Action form={form} {...submitButtonProps}>
|
|
141
|
+
{submitButtonProps?.children ?? "Submit"}
|
|
142
|
+
</Action>
|
|
143
|
+
)}
|
|
144
|
+
</Stack>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (skipFormElement) {
|
|
148
|
+
return content;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<form onSubmit={form.onSubmit} noValidate>
|
|
153
|
+
{content}
|
|
154
|
+
</form>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default TypeForm;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useInject } from "@alepha/react";
|
|
2
|
+
import { ToastService } from "../services/ToastService.tsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Use this hook to access the Toast Service for showing notifications.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const toast = useToast();
|
|
9
|
+
* toast.success({ message: "Operation completed successfully!" });
|
|
10
|
+
* toast.error({ title: "Error", message: "Something went wrong" });
|
|
11
|
+
*/
|
|
12
|
+
export const useToast = (): ToastService => {
|
|
13
|
+
return useInject(ToastService);
|
|
14
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { $module } from "@alepha/core";
|
|
2
2
|
import { AlephaReact } from "@alepha/react";
|
|
3
|
+
import type { ControlProps } from "./components/Control.tsx";
|
|
4
|
+
import { RootRouter } from "./RootRouter.ts";
|
|
5
|
+
import { ToastService } from "./services/ToastService.tsx";
|
|
3
6
|
|
|
4
7
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
export { Flex } from "@mantine/core";
|
|
10
|
+
export { default as Action } from "./components/Action.tsx";
|
|
11
|
+
export { default as AlephaMantineProvider } from "./components/AlephaMantineProvider.tsx";
|
|
12
|
+
export { default as Control } from "./components/Control.tsx";
|
|
13
|
+
export { default as ControlDate } from "./components/ControlDate.tsx";
|
|
14
|
+
export { default as ControlSelect } from "./components/ControlSelect.tsx";
|
|
15
|
+
export { default as DarkModeButton } from "./components/DarkModeButton.tsx";
|
|
16
|
+
export { default as Omnibar } from "./components/Omnibar.tsx";
|
|
17
|
+
export { default as TypeForm } from "./components/TypeForm.tsx";
|
|
18
|
+
export { useToast } from "./hooks/useToast.ts";
|
|
19
|
+
export * from "./RootRouter.ts";
|
|
20
|
+
export { ToastService } from "./services/ToastService.tsx";
|
|
21
|
+
export * from "./utils/icons.tsx";
|
|
22
|
+
export * from "./utils/string.ts";
|
|
9
23
|
|
|
10
24
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
declare module "typebox" {
|
|
27
|
+
interface TSchemaOptions {
|
|
28
|
+
$control?: Omit<ControlProps, "input">;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
17
33
|
|
|
@@ -21,6 +37,6 @@ export { default as Control } from "./components/Control";
|
|
|
21
37
|
* @module alepha.ui
|
|
22
38
|
*/
|
|
23
39
|
export const AlephaUI = $module({
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
name: "alepha.ui",
|
|
41
|
+
services: [AlephaReact, ToastService, RootRouter],
|
|
26
42
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { NotificationData } from "@mantine/notifications";
|
|
2
|
+
import { notifications } from "@mantine/notifications";
|
|
3
|
+
import {
|
|
4
|
+
IconAlertTriangle,
|
|
5
|
+
IconCheck,
|
|
6
|
+
IconInfoCircle,
|
|
7
|
+
IconX,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
|
|
10
|
+
export interface ToastServiceOptions {
|
|
11
|
+
default?: Partial<NotificationData>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ToastService {
|
|
15
|
+
protected readonly raw = notifications;
|
|
16
|
+
|
|
17
|
+
public readonly options: ToastServiceOptions = {
|
|
18
|
+
default: {
|
|
19
|
+
autoClose: 5000,
|
|
20
|
+
withCloseButton: true,
|
|
21
|
+
position: "top-center",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
public show(options: NotificationData) {
|
|
26
|
+
notifications.show({
|
|
27
|
+
...this.options.default,
|
|
28
|
+
...options,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public info(options: Partial<NotificationData>) {
|
|
33
|
+
this.show({
|
|
34
|
+
color: "blue",
|
|
35
|
+
icon: <IconInfoCircle size={20} />,
|
|
36
|
+
title: "Info",
|
|
37
|
+
message: "Information notification",
|
|
38
|
+
...options,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public success(options: Partial<NotificationData>) {
|
|
43
|
+
this.show({
|
|
44
|
+
color: "green",
|
|
45
|
+
icon: <IconCheck size={16} />,
|
|
46
|
+
title: "Success",
|
|
47
|
+
message: "Operation completed successfully",
|
|
48
|
+
...options,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public warning(options: Partial<NotificationData>) {
|
|
53
|
+
this.show({
|
|
54
|
+
color: "yellow",
|
|
55
|
+
icon: <IconAlertTriangle size={20} />,
|
|
56
|
+
title: "Warning",
|
|
57
|
+
message: "Please review this warning",
|
|
58
|
+
...options,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public danger(options: Partial<NotificationData>) {
|
|
63
|
+
this.show({
|
|
64
|
+
color: "red",
|
|
65
|
+
icon: <IconX size={20} />,
|
|
66
|
+
title: "Error",
|
|
67
|
+
message: "An error occurred",
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|