@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.
@@ -1,275 +1,290 @@
1
- import { TypeBoxError } from "@alepha/core";
2
- import { type InputField, useFormState } from "@alepha/react-form";
1
+ import { useFormState } from "@alepha/react-form";
3
2
  import {
4
- Autocomplete,
5
- type AutocompleteProps,
6
- Flex,
7
- Input,
8
- PasswordInput,
9
- type PasswordInputProps,
10
- SegmentedControl,
11
- type SegmentedControlProps,
12
- Select,
13
- type SelectProps,
14
- Switch,
15
- type SwitchProps,
16
- Textarea,
17
- type TextareaProps,
18
- TextInput,
19
- type TextInputProps,
3
+ ColorInput,
4
+ type ColorInputProps,
5
+ FileInput,
6
+ type FileInputProps,
7
+ Flex,
8
+ Input,
9
+ NumberInput,
10
+ type NumberInputProps,
11
+ PasswordInput,
12
+ type PasswordInputProps,
13
+ Switch,
14
+ type SwitchProps,
15
+ Textarea,
16
+ type TextareaProps,
17
+ TextInput,
18
+ type TextInputProps,
20
19
  } from "@mantine/core";
21
- import type { ComponentType, ReactNode } from "react";
20
+ import type {
21
+ DateInputProps,
22
+ DateTimePickerProps,
23
+ TimeInputProps,
24
+ } from "@mantine/dates";
25
+ import type { ComponentType } from "react";
26
+ import { type GenericControlProps, parseInput } from "../utils/parseInput.ts";
27
+ import ControlDate from "./ControlDate";
28
+ import ControlSelect, { type ControlSelectProps } from "./ControlSelect";
22
29
 
23
- export interface ControlProps {
24
- input: InputField;
25
-
26
- title?: string;
27
- description?: string;
28
-
29
- icon?: ReactNode;
30
-
31
- text?: TextInputProps;
32
- area?: boolean | TextareaProps;
33
- select?: boolean | SelectProps;
34
- autocomplete?: boolean | AutocompleteProps;
35
- password?: boolean | PasswordInputProps;
36
- switch?: boolean | SwitchProps;
37
- segmented?: boolean | Partial<SegmentedControlProps>;
38
-
39
- custom?: ComponentType<CustomControlProps>;
30
+ export interface ControlProps extends GenericControlProps {
31
+ text?: TextInputProps;
32
+ area?: boolean | TextareaProps;
33
+ select?: boolean | Partial<ControlSelectProps>;
34
+ password?: boolean | PasswordInputProps;
35
+ switch?: boolean | SwitchProps;
36
+ number?: boolean | NumberInputProps;
37
+ file?: boolean | FileInputProps;
38
+ color?: boolean | ColorInputProps;
39
+ date?: boolean | DateInputProps;
40
+ datetime?: boolean | DateTimePickerProps;
41
+ time?: boolean | TimeInputProps;
42
+ custom?: ComponentType<CustomControlProps>;
40
43
  }
41
44
 
42
45
  /**
43
46
  * Generic form control that renders the appropriate input based on the schema and props.
44
47
  *
45
48
  * Supports:
46
- * - TextInput
49
+ * - TextInput (with format detection: email, url, tel)
47
50
  * - Textarea
51
+ * - NumberInput (for number/integer types)
52
+ * - FileInput
53
+ * - ColorInput (for color format)
48
54
  * - Select (for enum types)
49
55
  * - Autocomplete
50
56
  * - PasswordInput
51
57
  * - Switch (for boolean types)
52
58
  * - SegmentedControl (for enum types)
59
+ * - DateInput (for date format)
60
+ * - DateTimePicker (for date-time format)
61
+ * - TimeInput (for time format)
53
62
  * - Custom component
54
63
  *
55
- * Automatically handles labels, descriptions, error messages, and required state.
64
+ * Automatically handles labels, descriptions, error messages, required state, and default icons.
56
65
  */
57
- const Control = (props: ControlProps) => {
58
- const form = useFormState(props.input);
59
- if (!props.input?.props) {
60
- return null;
61
- }
62
-
63
- // shared props
66
+ const Control = (_props: ControlProps) => {
67
+ const form = useFormState(_props.input, ["error"]);
68
+ const { inputProps, id, icon, format, schema } = parseInput(_props, form);
69
+ if (!_props.input?.props) {
70
+ return null;
71
+ }
64
72
 
65
- const disabled = false; // form.loading;
66
- const id = props.input.props.id;
67
- const label =
68
- props.title ??
69
- ("title" in props.input.schema &&
70
- typeof props.input.schema.title === "string"
71
- ? props.input.schema.title
72
- : undefined) ??
73
- prettyName(props.input.path);
74
- const description =
75
- props.description ??
76
- ("description" in props.input.schema &&
77
- typeof props.input.schema.description === "string"
78
- ? props.input.schema.description
79
- : undefined);
80
- const error =
81
- form.error && form.error instanceof TypeBoxError
82
- ? form.error.value.message
83
- : undefined;
84
- const icon = props.icon;
85
- const required = props.input.required;
73
+ const props = {
74
+ ..._props,
75
+ ...schema.$control,
76
+ };
86
77
 
87
- const inputProps = {
88
- label,
89
- description,
90
- error,
91
- required,
92
- disabled,
93
- };
78
+ //region <Custom/>
79
+ if (props.custom) {
80
+ const Custom = props.custom;
81
+ return (
82
+ <Input.Wrapper {...inputProps}>
83
+ <Flex flex={1} mt={"calc(var(--mantine-spacing-xs) / 2)"}>
84
+ <Custom
85
+ defaultValue={props.input.props.defaultValue}
86
+ onChange={(value) => {
87
+ props.input.set(value);
88
+ }}
89
+ />
90
+ </Flex>
91
+ </Input.Wrapper>
92
+ );
93
+ }
94
+ //endregion
94
95
 
95
- // -------------------------------------------------------------------------------------------------------------------
96
+ //region <NumberInput/>
97
+ if (
98
+ props.number ||
99
+ (props.input.schema &&
100
+ "type" in props.input.schema &&
101
+ (props.input.schema.type === "number" ||
102
+ props.input.schema.type === "integer"))
103
+ ) {
104
+ const numberInputProps =
105
+ typeof props.number === "object" ? props.number : {};
106
+ const { type, ...inputPropsWithoutType } = props.input.props;
107
+ return (
108
+ <NumberInput
109
+ {...inputProps}
110
+ id={id}
111
+ leftSection={icon}
112
+ {...inputPropsWithoutType}
113
+ {...numberInputProps}
114
+ />
115
+ );
116
+ }
117
+ //endregion
96
118
 
97
- if (props.custom) {
98
- const Custom = props.custom;
99
- return (
100
- <Input.Wrapper {...inputProps}>
101
- <Flex flex={1} mt={"calc(var(--mantine-spacing-xs) / 2)"}>
102
- <Custom
103
- defaultValue={props.input.props.defaultValue}
104
- onChange={(value) => {
105
- props.input.set(value);
106
- }}
107
- />
108
- </Flex>
109
- </Input.Wrapper>
110
- );
111
- }
119
+ //region <FileInput/>
120
+ if (props.file) {
121
+ const fileInputProps = typeof props.file === "object" ? props.file : {};
122
+ return (
123
+ <FileInput
124
+ {...inputProps}
125
+ id={id}
126
+ leftSection={icon}
127
+ onChange={(file) => {
128
+ props.input.set(file);
129
+ }}
130
+ {...fileInputProps}
131
+ />
132
+ );
133
+ }
134
+ //endregion
112
135
 
113
- // region <SegmentedControl/>
114
- if (props.segmented) {
115
- const segmentedControlProps: Partial<SegmentedControlProps> =
116
- typeof props.segmented === "object" ? props.segmented : {};
117
- const data =
118
- segmentedControlProps.data ??
119
- (props.input.schema &&
120
- "enum" in props.input.schema &&
121
- Array.isArray(props.input.schema.enum)
122
- ? props.input.schema.enum?.map((value: string) => ({
123
- value,
124
- label: value,
125
- }))
126
- : []);
127
- return (
128
- <Input.Wrapper {...inputProps}>
129
- <Flex mt={"calc(var(--mantine-spacing-xs) / 2)"}>
130
- <SegmentedControl
131
- disabled={disabled}
132
- defaultValue={String(props.input.props.defaultValue)}
133
- {...segmentedControlProps}
134
- onChange={(value) => {
135
- props.input.set(value);
136
- }}
137
- data={data}
138
- />
139
- </Flex>
140
- </Input.Wrapper>
141
- );
142
- }
143
- // endregion
136
+ //region <ColorInput/>
137
+ if (props.color || format === "color") {
138
+ const colorInputProps = typeof props.color === "object" ? props.color : {};
139
+ return (
140
+ <ColorInput
141
+ {...inputProps}
142
+ id={id}
143
+ leftSection={icon}
144
+ {...props.input.props}
145
+ {...colorInputProps}
146
+ />
147
+ );
148
+ }
149
+ //endregion
144
150
 
145
- // region <Autocomplete/>
146
- if (props.autocomplete) {
147
- const autocompleteProps =
148
- typeof props.autocomplete === "object" ? props.autocomplete : {};
151
+ //region <ControlSelect/>
152
+ // Handle: single enum, array of enum, array of strings, or explicit select/multi/tags props
153
+ const isEnum =
154
+ props.input.schema &&
155
+ "enum" in props.input.schema &&
156
+ props.input.schema.enum;
157
+ const isArray =
158
+ props.input.schema &&
159
+ "type" in props.input.schema &&
160
+ props.input.schema.type === "array";
149
161
 
150
- return (
151
- <Autocomplete
152
- {...inputProps}
153
- id={id}
154
- leftSection={icon}
155
- {...props.input.props}
156
- {...autocompleteProps}
157
- />
158
- );
159
- }
160
- // endregion
162
+ if (isEnum || isArray || props.select) {
163
+ const opts = typeof props.select === "object" ? props.select : {};
164
+ return (
165
+ <ControlSelect
166
+ input={props.input}
167
+ title={props.title}
168
+ description={props.description}
169
+ icon={icon}
170
+ {...opts}
171
+ />
172
+ );
173
+ }
174
+ //endregion
161
175
 
162
- // region <Select/>
163
- if (
164
- (props.input.schema &&
165
- "enum" in props.input.schema &&
166
- props.input.schema.enum) ||
167
- props.select
168
- ) {
169
- const data =
170
- props.input.schema &&
171
- "enum" in props.input.schema &&
172
- Array.isArray(props.input.schema.enum)
173
- ? props.input.schema.enum?.map((value: string) => ({
174
- value,
175
- label: value,
176
- }))
177
- : [];
176
+ //region <Switch/>
177
+ if (
178
+ (props.input.schema &&
179
+ "type" in props.input.schema &&
180
+ props.input.schema.type === "boolean") ||
181
+ props.switch
182
+ ) {
183
+ const switchProps = typeof props.switch === "object" ? props.switch : {};
178
184
 
179
- const selectProps = typeof props.select === "object" ? props.select : {};
185
+ return (
186
+ <Switch
187
+ {...inputProps}
188
+ id={id}
189
+ color={"blue"}
190
+ defaultChecked={props.input.props.defaultValue}
191
+ {...props.input.props}
192
+ {...switchProps}
193
+ />
194
+ );
195
+ }
196
+ //endregion
180
197
 
181
- return (
182
- <Select
183
- {...inputProps}
184
- id={id}
185
- leftSection={icon}
186
- data={data}
187
- {...props.input.props}
188
- {...selectProps}
189
- />
190
- );
191
- }
192
- // endregion
198
+ //region <PasswordInput/>
199
+ if (props.password || props.input.props.name?.includes("password")) {
200
+ const passwordInputProps =
201
+ typeof props.password === "object" ? props.password : {};
202
+ return (
203
+ <PasswordInput
204
+ {...inputProps}
205
+ id={id}
206
+ leftSection={icon}
207
+ {...props.input.props}
208
+ {...passwordInputProps}
209
+ />
210
+ );
211
+ }
212
+ //endregion
193
213
 
194
- // region <Switch/>
214
+ //region <Textarea/>
215
+ if (props.area) {
216
+ const textAreaProps = typeof props.area === "object" ? props.area : {};
217
+ return (
218
+ <Textarea
219
+ {...inputProps}
220
+ id={id}
221
+ leftSection={icon}
222
+ {...props.input.props}
223
+ {...textAreaProps}
224
+ />
225
+ );
226
+ }
227
+ //endregion
195
228
 
196
- if (
197
- (props.input.schema &&
198
- "type" in props.input.schema &&
199
- props.input.schema.type === "boolean") ||
200
- props.switch
201
- ) {
202
- const switchProps = typeof props.switch === "object" ? props.switch : {};
229
+ //region <ControlDate/>
230
+ // Handle: date, date-time, and time formats
231
+ if (
232
+ props.date ||
233
+ props.datetime ||
234
+ props.time ||
235
+ format === "date" ||
236
+ format === "date-time" ||
237
+ format === "time"
238
+ ) {
239
+ return (
240
+ <ControlDate
241
+ input={props.input}
242
+ title={props.title}
243
+ description={props.description}
244
+ icon={icon}
245
+ date={props.date}
246
+ datetime={props.datetime}
247
+ time={props.time}
248
+ />
249
+ );
250
+ }
251
+ //endregion
203
252
 
204
- return (
205
- <Switch
206
- {...inputProps}
207
- id={id}
208
- color={"blue"}
209
- defaultChecked={props.input.props.defaultValue}
210
- {...props.input.props}
211
- {...switchProps}
212
- />
213
- );
214
- }
215
- // endregion
253
+ //region <TextInput/> with format detection
254
+ const textInputProps = typeof props.text === "object" ? props.text : {};
216
255
 
217
- // region <PasswordInput/>
218
- if (props.password) {
219
- const passwordInputProps =
220
- typeof props.password === "object" ? props.password : {};
221
- return (
222
- <PasswordInput
223
- {...inputProps}
224
- id={id}
225
- leftSection={icon}
226
- {...props.input.props}
227
- {...passwordInputProps}
228
- />
229
- );
230
- }
231
- //endregion
256
+ // Detect HTML5 input type from format
257
+ const getInputType = (): string | undefined => {
258
+ switch (format) {
259
+ case "email":
260
+ return "email";
261
+ case "url":
262
+ case "uri":
263
+ return "url";
264
+ case "tel":
265
+ case "phone":
266
+ return "tel";
267
+ default:
268
+ return undefined;
269
+ }
270
+ };
232
271
 
233
- //region <Textarea/>
234
- if (props.area) {
235
- const textAreaProps = typeof props.area === "object" ? props.area : {};
236
- return (
237
- <Textarea
238
- {...inputProps}
239
- id={id}
240
- leftSection={icon}
241
- {...props.input.props}
242
- {...textAreaProps}
243
- />
244
- );
245
- }
246
- //endregion
247
-
248
- // region <TextInput/>
249
- const textInputProps = typeof props.text === "object" ? props.text : {};
250
- return (
251
- <TextInput
252
- {...inputProps}
253
- id={id}
254
- leftSection={icon}
255
- {...props.input.props}
256
- {...textInputProps}
257
- />
258
- );
259
- //endregion
272
+ return (
273
+ <TextInput
274
+ {...inputProps}
275
+ id={id}
276
+ leftSection={icon}
277
+ type={getInputType()}
278
+ {...props.input.props}
279
+ {...textInputProps}
280
+ />
281
+ );
282
+ //endregion
260
283
  };
261
284
 
262
285
  export default Control;
263
286
 
264
- const prettyName = (name: string) => {
265
- return capitalize(name.replaceAll("/", ""));
266
- };
267
-
268
- const capitalize = (str: string) => {
269
- return str.charAt(0).toUpperCase() + str.slice(1);
270
- };
271
-
272
287
  export type CustomControlProps = {
273
- defaultValue: any;
274
- onChange: (value: any) => void;
288
+ defaultValue: any;
289
+ onChange: (value: any) => void;
275
290
  };
@@ -0,0 +1,104 @@
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 "../utils/parseInput.ts";
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, format } = parseInput(props, form);
31
+ if (!props.input?.props) {
32
+ return null;
33
+ }
34
+
35
+ // region <DateTimePicker/>
36
+ if (props.datetime || format === "date-time") {
37
+ const dateTimePickerProps =
38
+ typeof props.datetime === "object" ? props.datetime : {};
39
+ return (
40
+ <DateTimePicker
41
+ {...inputProps}
42
+ id={id}
43
+ leftSection={icon}
44
+ defaultValue={
45
+ props.input.props.defaultValue
46
+ ? new Date(props.input.props.defaultValue)
47
+ : undefined
48
+ }
49
+ onChange={(value) => {
50
+ props.input.set(value ? new Date(value).toISOString() : undefined);
51
+ }}
52
+ {...dateTimePickerProps}
53
+ />
54
+ );
55
+ }
56
+ //endregion
57
+
58
+ // region <DateInput/>
59
+ if (props.date || format === "date") {
60
+ const dateInputProps = typeof props.date === "object" ? props.date : {};
61
+ return (
62
+ <DateInput
63
+ {...inputProps}
64
+ id={id}
65
+ leftSection={icon}
66
+ defaultValue={
67
+ props.input.props.defaultValue
68
+ ? new Date(props.input.props.defaultValue)
69
+ : undefined
70
+ }
71
+ onChange={(value) => {
72
+ props.input.set(
73
+ value ? new Date(value).toISOString().slice(0, 10) : undefined,
74
+ );
75
+ }}
76
+ {...dateInputProps}
77
+ />
78
+ );
79
+ }
80
+ //endregion
81
+
82
+ // region <TimeInput/>
83
+ if (props.time || format === "time") {
84
+ const timeInputProps = typeof props.time === "object" ? props.time : {};
85
+ return (
86
+ <TimeInput
87
+ {...inputProps}
88
+ id={id}
89
+ leftSection={icon}
90
+ defaultValue={props.input.props.defaultValue}
91
+ onChange={(event) => {
92
+ props.input.set(event.currentTarget.value);
93
+ }}
94
+ {...timeInputProps}
95
+ />
96
+ );
97
+ }
98
+ //endregion
99
+
100
+ // Fallback - shouldn't happen
101
+ return null;
102
+ };
103
+
104
+ export default ControlDate;