@mihcm/ui 0.14.1 → 0.15.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/CheckboxGrid.native.d.ts.map +1 -1
- package/dist/CheckboxGrid.native.js +2 -1
- package/dist/CheckboxGrid.native.js.map +1 -1
- package/dist/Combobox.native.d.ts.map +1 -1
- package/dist/Combobox.native.js +2 -1
- package/dist/Combobox.native.js.map +1 -1
- package/dist/DataTable/column-filter.d.ts +8 -0
- package/dist/DataTable/column-filter.d.ts.map +1 -0
- package/dist/DataTable/column-filter.js +67 -0
- package/dist/DataTable/column-filter.js.map +1 -0
- package/dist/DataTable/column-header.d.ts +16 -0
- package/dist/DataTable/column-header.d.ts.map +1 -0
- package/dist/DataTable/column-header.js +11 -0
- package/dist/DataTable/column-header.js.map +1 -0
- package/dist/DataTable/column-visibility.d.ts +7 -0
- package/dist/DataTable/column-visibility.d.ts.map +1 -0
- package/dist/DataTable/column-visibility.js +35 -0
- package/dist/DataTable/column-visibility.js.map +1 -0
- package/dist/DataTable/index.d.ts +5 -0
- package/dist/DataTable/index.d.ts.map +1 -0
- package/dist/DataTable/index.js +5 -0
- package/dist/DataTable/index.js.map +1 -0
- package/dist/DataTable/pinning.d.ts +13 -0
- package/dist/DataTable/pinning.d.ts.map +1 -0
- package/dist/DataTable/pinning.js +29 -0
- package/dist/DataTable/pinning.js.map +1 -0
- package/dist/DataTable.d.ts +3 -7
- package/dist/DataTable.d.ts.map +1 -1
- package/dist/DataTable.js +7 -126
- package/dist/DataTable.js.map +1 -1
- package/dist/Dialog.native.d.ts +3 -1
- package/dist/Dialog.native.d.ts.map +1 -1
- package/dist/Dialog.native.js +2 -2
- package/dist/Dialog.native.js.map +1 -1
- package/dist/Form/building-blocks.d.ts +26 -0
- package/dist/Form/building-blocks.d.ts.map +1 -0
- package/dist/Form/building-blocks.js +29 -0
- package/dist/Form/building-blocks.js.map +1 -0
- package/dist/Form/fields-choice.d.ts +72 -0
- package/dist/Form/fields-choice.d.ts.map +1 -0
- package/dist/Form/fields-choice.js +69 -0
- package/dist/Form/fields-choice.js.map +1 -0
- package/dist/Form/fields-complex.d.ts +28 -0
- package/dist/Form/fields-complex.d.ts.map +1 -0
- package/dist/Form/fields-complex.js +38 -0
- package/dist/Form/fields-complex.js.map +1 -0
- package/dist/Form/fields-date.d.ts +46 -0
- package/dist/Form/fields-date.d.ts.map +1 -0
- package/dist/Form/fields-date.js +41 -0
- package/dist/Form/fields-date.js.map +1 -0
- package/dist/Form/fields-text.d.ts +47 -0
- package/dist/Form/fields-text.d.ts.map +1 -0
- package/dist/Form/fields-text.js +46 -0
- package/dist/Form/fields-text.js.map +1 -0
- package/dist/Form/fields-toggle.d.ts +24 -0
- package/dist/Form/fields-toggle.d.ts.map +1 -0
- package/dist/Form/fields-toggle.js +32 -0
- package/dist/Form/fields-toggle.js.map +1 -0
- package/dist/Form/helpers.d.ts +66 -0
- package/dist/Form/helpers.d.ts.map +1 -0
- package/dist/Form/helpers.js +44 -0
- package/dist/Form/helpers.js.map +1 -0
- package/dist/Form/types.d.ts +25 -0
- package/dist/Form/types.d.ts.map +1 -0
- package/dist/Form/types.js +8 -0
- package/dist/Form/types.js.map +1 -0
- package/dist/Form.d.ts +24 -298
- package/dist/Form.d.ts.map +1 -1
- package/dist/Form.js +30 -246
- package/dist/Form.js.map +1 -1
- package/dist/IconSidebar.d.ts +6 -46
- package/dist/IconSidebar.d.ts.map +1 -1
- package/dist/IconSidebar.js +6 -116
- package/dist/IconSidebar.js.map +1 -1
- package/dist/MainSidebar/back-button.d.ts +14 -0
- package/dist/MainSidebar/back-button.d.ts.map +1 -0
- package/dist/MainSidebar/back-button.js +14 -0
- package/dist/MainSidebar/back-button.js.map +1 -0
- package/dist/MainSidebar/breadcrumb.d.ts +10 -0
- package/dist/MainSidebar/breadcrumb.d.ts.map +1 -0
- package/dist/MainSidebar/breadcrumb.js +24 -0
- package/dist/MainSidebar/breadcrumb.js.map +1 -0
- package/dist/MainSidebar/columns.d.ts +3 -0
- package/dist/MainSidebar/columns.d.ts.map +1 -0
- package/dist/MainSidebar/columns.js +198 -0
- package/dist/MainSidebar/columns.js.map +1 -0
- package/dist/MainSidebar/command.d.ts +3 -0
- package/dist/MainSidebar/command.d.ts.map +1 -0
- package/dist/MainSidebar/command.js +193 -0
- package/dist/MainSidebar/command.js.map +1 -0
- package/dist/MainSidebar/drilldown.d.ts +3 -0
- package/dist/MainSidebar/drilldown.d.ts.map +1 -0
- package/dist/MainSidebar/drilldown.js +154 -0
- package/dist/MainSidebar/drilldown.js.map +1 -0
- package/dist/MainSidebar/expanded.d.ts +7 -0
- package/dist/MainSidebar/expanded.d.ts.map +1 -0
- package/dist/MainSidebar/expanded.js +102 -0
- package/dist/MainSidebar/expanded.js.map +1 -0
- package/dist/MainSidebar/floating.d.ts +3 -0
- package/dist/MainSidebar/floating.d.ts.map +1 -0
- package/dist/MainSidebar/floating.js +116 -0
- package/dist/MainSidebar/floating.js.map +1 -0
- package/dist/MainSidebar/helpers.d.ts +50 -0
- package/dist/MainSidebar/helpers.d.ts.map +1 -0
- package/dist/MainSidebar/helpers.js +148 -0
- package/dist/MainSidebar/helpers.js.map +1 -0
- package/dist/MainSidebar/hover.d.ts +3 -0
- package/dist/MainSidebar/hover.d.ts.map +1 -0
- package/dist/MainSidebar/hover.js +177 -0
- package/dist/MainSidebar/hover.js.map +1 -0
- package/dist/MainSidebar/index.d.ts +6 -0
- package/dist/MainSidebar/index.d.ts.map +1 -0
- package/dist/MainSidebar/index.js +108 -0
- package/dist/MainSidebar/index.js.map +1 -0
- package/dist/MainSidebar/mobile.d.ts +29 -0
- package/dist/MainSidebar/mobile.d.ts.map +1 -0
- package/dist/MainSidebar/mobile.js +38 -0
- package/dist/MainSidebar/mobile.js.map +1 -0
- package/dist/MainSidebar/motion.d.ts +23 -0
- package/dist/MainSidebar/motion.d.ts.map +1 -0
- package/dist/MainSidebar/motion.js +40 -0
- package/dist/MainSidebar/motion.js.map +1 -0
- package/dist/MainSidebar/rail.d.ts +24 -0
- package/dist/MainSidebar/rail.d.ts.map +1 -0
- package/dist/MainSidebar/rail.js +29 -0
- package/dist/MainSidebar/rail.js.map +1 -0
- package/dist/MainSidebar/search.d.ts +19 -0
- package/dist/MainSidebar/search.d.ts.map +1 -0
- package/dist/MainSidebar/search.js +33 -0
- package/dist/MainSidebar/search.js.map +1 -0
- package/dist/MainSidebar/types.d.ts +161 -0
- package/dist/MainSidebar/types.d.ts.map +1 -0
- package/dist/MainSidebar/types.js +2 -0
- package/dist/MainSidebar/types.js.map +1 -0
- package/dist/MainSidebar.d.ts +6 -1
- package/dist/MainSidebar.d.ts.map +1 -1
- package/dist/MainSidebar.js +6 -1
- package/dist/MainSidebar.js.map +1 -1
- package/dist/NavigationMenu.js +1 -1
- package/dist/NavigationMenu.js.map +1 -1
- package/dist/RichTextEditor/theme.d.ts +44 -0
- package/dist/RichTextEditor/theme.d.ts.map +1 -0
- package/dist/RichTextEditor/theme.js +41 -0
- package/dist/RichTextEditor/theme.js.map +1 -0
- package/dist/RichTextEditor/toolbar-icons.d.ts +21 -0
- package/dist/RichTextEditor/toolbar-icons.d.ts.map +1 -0
- package/dist/RichTextEditor/toolbar-icons.js +21 -0
- package/dist/RichTextEditor/toolbar-icons.js.map +1 -0
- package/dist/RichTextEditor/toolbar.d.ts +5 -0
- package/dist/RichTextEditor/toolbar.d.ts.map +1 -0
- package/dist/RichTextEditor/toolbar.js +116 -0
- package/dist/RichTextEditor/toolbar.js.map +1 -0
- package/dist/RichTextEditor.d.ts +16 -9
- package/dist/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor.js +18 -164
- package/dist/RichTextEditor.js.map +1 -1
- package/dist/Select/content.d.ts +9 -0
- package/dist/Select/content.d.ts.map +1 -0
- package/dist/Select/content.js +80 -0
- package/dist/Select/content.js.map +1 -0
- package/dist/Select/context.d.ts +27 -0
- package/dist/Select/context.d.ts.map +1 -0
- package/dist/Select/context.js +35 -0
- package/dist/Select/context.js.map +1 -0
- package/dist/Select/item.d.ts +13 -0
- package/dist/Select/item.d.ts.map +1 -0
- package/dist/Select/item.js +39 -0
- package/dist/Select/item.js.map +1 -0
- package/dist/Select/parts.d.ts +14 -0
- package/dist/Select/parts.d.ts.map +1 -0
- package/dist/Select/parts.js +17 -0
- package/dist/Select/parts.js.map +1 -0
- package/dist/Select/react-select.d.ts +25 -0
- package/dist/Select/react-select.d.ts.map +1 -0
- package/dist/Select/react-select.js +66 -0
- package/dist/Select/react-select.js.map +1 -0
- package/dist/Select/root.d.ts +15 -0
- package/dist/Select/root.d.ts.map +1 -0
- package/dist/Select/root.js +41 -0
- package/dist/Select/root.js.map +1 -0
- package/dist/Select/trigger.d.ts +15 -0
- package/dist/Select/trigger.d.ts.map +1 -0
- package/dist/Select/trigger.js +61 -0
- package/dist/Select/trigger.js.map +1 -0
- package/dist/Select.d.ts +14 -62
- package/dist/Select.d.ts.map +1 -1
- package/dist/Select.js +14 -293
- package/dist/Select.js.map +1 -1
- package/dist/Sidebar/context.d.ts +28 -0
- package/dist/Sidebar/context.d.ts.map +1 -0
- package/dist/Sidebar/context.js +37 -0
- package/dist/Sidebar/context.js.map +1 -0
- package/dist/Sidebar/group.d.ts +13 -0
- package/dist/Sidebar/group.d.ts.map +1 -0
- package/dist/Sidebar/group.js +20 -0
- package/dist/Sidebar/group.js.map +1 -0
- package/dist/Sidebar/icons.d.ts +7 -0
- package/dist/Sidebar/icons.d.ts.map +1 -0
- package/dist/Sidebar/icons.js +12 -0
- package/dist/Sidebar/icons.js.map +1 -0
- package/dist/Sidebar/layout.d.ts +9 -0
- package/dist/Sidebar/layout.d.ts.map +1 -0
- package/dist/Sidebar/layout.js +21 -0
- package/dist/Sidebar/layout.js.map +1 -0
- package/dist/Sidebar/menu.d.ts +29 -0
- package/dist/Sidebar/menu.d.ts.map +1 -0
- package/dist/Sidebar/menu.js +55 -0
- package/dist/Sidebar/menu.js.map +1 -0
- package/dist/Sidebar/provider.d.ts +33 -0
- package/dist/Sidebar/provider.d.ts.map +1 -0
- package/dist/Sidebar/provider.js +110 -0
- package/dist/Sidebar/provider.js.map +1 -0
- package/dist/Sidebar/sidebar.d.ts +17 -0
- package/dist/Sidebar/sidebar.d.ts.map +1 -0
- package/dist/Sidebar/sidebar.js +51 -0
- package/dist/Sidebar/sidebar.js.map +1 -0
- package/dist/Sidebar/submenu.d.ts +13 -0
- package/dist/Sidebar/submenu.d.ts.map +1 -0
- package/dist/Sidebar/submenu.js +17 -0
- package/dist/Sidebar/submenu.js.map +1 -0
- package/dist/Sidebar/trigger.d.ts +9 -0
- package/dist/Sidebar/trigger.d.ts.map +1 -0
- package/dist/Sidebar/trigger.js +33 -0
- package/dist/Sidebar/trigger.js.map +1 -0
- package/dist/Sidebar.d.ts +14 -104
- package/dist/Sidebar.d.ts.map +1 -1
- package/dist/Sidebar.js +14 -300
- package/dist/Sidebar.js.map +1 -1
- package/dist/StatCard.d.ts +67 -9
- package/dist/StatCard.d.ts.map +1 -1
- package/dist/StatCard.js +111 -9
- package/dist/StatCard.js.map +1 -1
- package/dist/TransferList.native.d.ts.map +1 -1
- package/dist/TransferList.native.js +2 -1
- package/dist/TransferList.native.js.map +1 -1
- package/package.json +2 -2
- package/src/CheckboxGrid.native.tsx +2 -1
- package/src/Combobox.native.tsx +2 -1
- package/src/DataTable/column-filter.tsx +134 -0
- package/src/DataTable/column-header.tsx +67 -0
- package/src/DataTable/column-visibility.tsx +87 -0
- package/src/DataTable/index.ts +4 -0
- package/src/DataTable/pinning.ts +40 -0
- package/src/DataTable.tsx +14 -297
- package/src/Dialog.native.tsx +4 -2
- package/src/Form/building-blocks.tsx +97 -0
- package/src/Form/fields-choice.tsx +312 -0
- package/src/Form/fields-complex.tsx +195 -0
- package/src/Form/fields-date.tsx +195 -0
- package/src/Form/fields-text.tsx +218 -0
- package/src/Form/fields-toggle.tsx +123 -0
- package/src/Form/helpers.tsx +189 -0
- package/src/Form/types.ts +26 -0
- package/src/Form.tsx +91 -1308
- package/src/IconSidebar.tsx +20 -442
- package/src/MainSidebar/back-button.tsx +58 -0
- package/src/MainSidebar/breadcrumb.tsx +53 -0
- package/src/MainSidebar/columns.tsx +350 -0
- package/src/MainSidebar/command.tsx +404 -0
- package/src/MainSidebar/drilldown.tsx +373 -0
- package/src/MainSidebar/expanded.tsx +414 -0
- package/src/MainSidebar/floating.tsx +268 -0
- package/src/MainSidebar/helpers.ts +164 -0
- package/src/MainSidebar/hover.tsx +334 -0
- package/src/MainSidebar/index.tsx +191 -0
- package/src/MainSidebar/mobile.tsx +117 -0
- package/src/MainSidebar/motion.ts +64 -0
- package/src/MainSidebar/rail.tsx +137 -0
- package/src/MainSidebar/search.tsx +99 -0
- package/src/MainSidebar/types.ts +208 -0
- package/src/MainSidebar.tsx +15 -4
- package/src/NavigationMenu.tsx +1 -1
- package/src/RichTextEditor/theme.ts +43 -0
- package/src/RichTextEditor/toolbar-icons.tsx +40 -0
- package/src/RichTextEditor/toolbar.tsx +271 -0
- package/src/RichTextEditor.tsx +23 -371
- package/src/Select/content.tsx +111 -0
- package/src/Select/context.tsx +66 -0
- package/src/Select/item.tsx +97 -0
- package/src/Select/parts.tsx +43 -0
- package/src/Select/react-select.tsx +216 -0
- package/src/Select/root.tsx +75 -0
- package/src/Select/trigger.tsx +122 -0
- package/src/Select.tsx +34 -692
- package/src/Sidebar/context.tsx +72 -0
- package/src/Sidebar/group.tsx +69 -0
- package/src/Sidebar/icons.tsx +42 -0
- package/src/Sidebar/layout.tsx +64 -0
- package/src/Sidebar/menu.tsx +171 -0
- package/src/Sidebar/provider.tsx +224 -0
- package/src/Sidebar/sidebar.tsx +178 -0
- package/src/Sidebar/submenu.tsx +58 -0
- package/src/Sidebar/trigger.tsx +104 -0
- package/src/Sidebar.tsx +44 -927
- package/src/StatCard.tsx +365 -20
- package/src/TransferList.native.tsx +2 -1
- package/dist/TiptapEditor.d.ts +0 -24
- package/dist/TiptapEditor.d.ts.map +0 -1
- package/dist/TiptapEditor.js +0 -84
- package/dist/TiptapEditor.js.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Text-style fields integrated with TanStack Form: FormInput, FormSearchField,
|
|
5
|
+
* FormTextarea. Each wires `form.Field` to the matching UI primitive with
|
|
6
|
+
* label, description, and error display included.
|
|
7
|
+
*/
|
|
8
|
+
import { useId } from 'react';
|
|
9
|
+
import { Input } from '../Input.js';
|
|
10
|
+
import { SearchField, type SearchFieldProps } from '../SearchField.js';
|
|
11
|
+
import { Textarea } from '../Textarea.js';
|
|
12
|
+
import {
|
|
13
|
+
FormDescription,
|
|
14
|
+
FormItem,
|
|
15
|
+
FormLabel,
|
|
16
|
+
FormMessage,
|
|
17
|
+
} from './building-blocks.js';
|
|
18
|
+
import type { AnyFormApi, FieldRenderProps, FormFieldValidators } from './types.js';
|
|
19
|
+
|
|
20
|
+
/* ── FormInput ─────────────────────────────────────────────────────── */
|
|
21
|
+
|
|
22
|
+
export interface FormInputProps {
|
|
23
|
+
form: AnyFormApi;
|
|
24
|
+
name: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
validators?: FormFieldValidators;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Input field integrated with TanStack Form. */
|
|
36
|
+
export function FormInput({
|
|
37
|
+
form,
|
|
38
|
+
name,
|
|
39
|
+
label,
|
|
40
|
+
description,
|
|
41
|
+
required,
|
|
42
|
+
placeholder,
|
|
43
|
+
type,
|
|
44
|
+
disabled,
|
|
45
|
+
className,
|
|
46
|
+
validators,
|
|
47
|
+
}: FormInputProps) {
|
|
48
|
+
const id = useId();
|
|
49
|
+
const descId = `${id}-desc`;
|
|
50
|
+
const msgId = `${id}-msg`;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<form.Field name={name} {...(validators !== undefined && { validators })}>
|
|
54
|
+
{(field: FieldRenderProps<string>) => {
|
|
55
|
+
const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
|
|
56
|
+
const hasError = errors.length > 0;
|
|
57
|
+
return (
|
|
58
|
+
<FormItem {...(className !== undefined && { className })}>
|
|
59
|
+
<FormLabel htmlFor={id} {...(required !== undefined && { required })}>
|
|
60
|
+
{label}
|
|
61
|
+
</FormLabel>
|
|
62
|
+
<Input
|
|
63
|
+
id={id}
|
|
64
|
+
{...(type !== undefined && { type })}
|
|
65
|
+
{...(placeholder !== undefined && { placeholder })}
|
|
66
|
+
{...(disabled !== undefined && { disabled })}
|
|
67
|
+
{...(required !== undefined && { required })}
|
|
68
|
+
invalid={hasError}
|
|
69
|
+
value={field.state.value ?? ''}
|
|
70
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
71
|
+
onBlur={field.handleBlur}
|
|
72
|
+
aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
|
|
73
|
+
/>
|
|
74
|
+
{description !== undefined ? (
|
|
75
|
+
<FormDescription id={descId}>{description}</FormDescription>
|
|
76
|
+
) : null}
|
|
77
|
+
{hasError ? <FormMessage id={msgId} errors={errors} /> : null}
|
|
78
|
+
</FormItem>
|
|
79
|
+
);
|
|
80
|
+
}}
|
|
81
|
+
</form.Field>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── FormSearchField ─────────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
export interface FormSearchFieldProps {
|
|
88
|
+
form: AnyFormApi;
|
|
89
|
+
name: string;
|
|
90
|
+
label: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
required?: boolean;
|
|
93
|
+
placeholder?: string;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
size?: SearchFieldProps['size'];
|
|
96
|
+
noClear?: boolean;
|
|
97
|
+
className?: string;
|
|
98
|
+
inputClassName?: string;
|
|
99
|
+
validators?: FormFieldValidators;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Search field integrated with TanStack Form. */
|
|
103
|
+
export function FormSearchField({
|
|
104
|
+
form,
|
|
105
|
+
name,
|
|
106
|
+
label,
|
|
107
|
+
description,
|
|
108
|
+
required,
|
|
109
|
+
placeholder,
|
|
110
|
+
disabled,
|
|
111
|
+
size,
|
|
112
|
+
noClear,
|
|
113
|
+
className,
|
|
114
|
+
inputClassName,
|
|
115
|
+
validators,
|
|
116
|
+
}: FormSearchFieldProps) {
|
|
117
|
+
const id = useId();
|
|
118
|
+
const descId = `${id}-desc`;
|
|
119
|
+
const msgId = `${id}-msg`;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<form.Field name={name} {...(validators !== undefined && { validators })}>
|
|
123
|
+
{(field: FieldRenderProps<string>) => {
|
|
124
|
+
const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
|
|
125
|
+
const hasError = errors.length > 0;
|
|
126
|
+
return (
|
|
127
|
+
<FormItem {...(className !== undefined && { className })}>
|
|
128
|
+
<FormLabel htmlFor={id} {...(required !== undefined && { required })}>
|
|
129
|
+
{label}
|
|
130
|
+
</FormLabel>
|
|
131
|
+
<SearchField
|
|
132
|
+
id={id}
|
|
133
|
+
value={field.state.value ?? ''}
|
|
134
|
+
onValueChange={field.handleChange}
|
|
135
|
+
onBlur={field.handleBlur}
|
|
136
|
+
{...(placeholder !== undefined && { placeholder })}
|
|
137
|
+
{...(disabled !== undefined && { disabled })}
|
|
138
|
+
{...(size !== undefined && { size })}
|
|
139
|
+
{...(noClear !== undefined && { noClear })}
|
|
140
|
+
{...(inputClassName !== undefined && { inputClassName })}
|
|
141
|
+
aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
|
|
142
|
+
aria-invalid={hasError}
|
|
143
|
+
/>
|
|
144
|
+
{description !== undefined ? (
|
|
145
|
+
<FormDescription id={descId}>{description}</FormDescription>
|
|
146
|
+
) : null}
|
|
147
|
+
{hasError ? <FormMessage id={msgId} errors={errors} /> : null}
|
|
148
|
+
</FormItem>
|
|
149
|
+
);
|
|
150
|
+
}}
|
|
151
|
+
</form.Field>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── FormTextarea ────────────────────────────────────────────────── */
|
|
156
|
+
|
|
157
|
+
export interface FormTextareaProps {
|
|
158
|
+
form: AnyFormApi;
|
|
159
|
+
name: string;
|
|
160
|
+
label: string;
|
|
161
|
+
description?: string;
|
|
162
|
+
required?: boolean;
|
|
163
|
+
placeholder?: string;
|
|
164
|
+
rows?: number;
|
|
165
|
+
disabled?: boolean;
|
|
166
|
+
className?: string;
|
|
167
|
+
validators?: FormFieldValidators;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Textarea field integrated with TanStack Form. */
|
|
171
|
+
export function FormTextarea({
|
|
172
|
+
form,
|
|
173
|
+
name,
|
|
174
|
+
label,
|
|
175
|
+
description,
|
|
176
|
+
required,
|
|
177
|
+
placeholder,
|
|
178
|
+
rows,
|
|
179
|
+
disabled,
|
|
180
|
+
className,
|
|
181
|
+
validators,
|
|
182
|
+
}: FormTextareaProps) {
|
|
183
|
+
const id = useId();
|
|
184
|
+
const descId = `${id}-desc`;
|
|
185
|
+
const msgId = `${id}-msg`;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<form.Field name={name} {...(validators !== undefined && { validators })}>
|
|
189
|
+
{(field: FieldRenderProps<string>) => {
|
|
190
|
+
const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
|
|
191
|
+
const hasError = errors.length > 0;
|
|
192
|
+
return (
|
|
193
|
+
<FormItem {...(className !== undefined && { className })}>
|
|
194
|
+
<FormLabel htmlFor={id} {...(required !== undefined && { required })}>
|
|
195
|
+
{label}
|
|
196
|
+
</FormLabel>
|
|
197
|
+
<Textarea
|
|
198
|
+
id={id}
|
|
199
|
+
{...(placeholder !== undefined && { placeholder })}
|
|
200
|
+
{...(disabled !== undefined && { disabled })}
|
|
201
|
+
{...(required !== undefined && { required })}
|
|
202
|
+
{...(rows !== undefined && { rows })}
|
|
203
|
+
invalid={hasError}
|
|
204
|
+
value={field.state.value ?? ''}
|
|
205
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
206
|
+
onBlur={field.handleBlur}
|
|
207
|
+
aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
|
|
208
|
+
/>
|
|
209
|
+
{description !== undefined ? (
|
|
210
|
+
<FormDescription id={descId}>{description}</FormDescription>
|
|
211
|
+
) : null}
|
|
212
|
+
{hasError ? <FormMessage id={msgId} errors={errors} /> : null}
|
|
213
|
+
</FormItem>
|
|
214
|
+
);
|
|
215
|
+
}}
|
|
216
|
+
</form.Field>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Boolean / toggle fields integrated with TanStack Form: FormCheckbox, FormSwitch.
|
|
5
|
+
*/
|
|
6
|
+
import { useId } from 'react';
|
|
7
|
+
import { Checkbox } from '../Checkbox.js';
|
|
8
|
+
import { Switch } from '../Switch.js';
|
|
9
|
+
import {
|
|
10
|
+
FormDescription,
|
|
11
|
+
FormItem,
|
|
12
|
+
FormLabel,
|
|
13
|
+
FormMessage,
|
|
14
|
+
} from './building-blocks.js';
|
|
15
|
+
import type { AnyFormApi, FieldRenderProps, FormFieldValidators } from './types.js';
|
|
16
|
+
|
|
17
|
+
/* ── FormCheckbox ────────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
export interface FormCheckboxProps {
|
|
20
|
+
form: AnyFormApi;
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
validators?: FormFieldValidators;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Checkbox field integrated with TanStack Form. */
|
|
30
|
+
export function FormCheckbox({
|
|
31
|
+
form,
|
|
32
|
+
name,
|
|
33
|
+
label,
|
|
34
|
+
description,
|
|
35
|
+
disabled,
|
|
36
|
+
className,
|
|
37
|
+
validators,
|
|
38
|
+
}: FormCheckboxProps) {
|
|
39
|
+
const id = useId();
|
|
40
|
+
const descId = `${id}-desc`;
|
|
41
|
+
const msgId = `${id}-msg`;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<form.Field name={name} {...(validators !== undefined && { validators })}>
|
|
45
|
+
{(field: FieldRenderProps<boolean>) => {
|
|
46
|
+
const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
|
|
47
|
+
const hasError = errors.length > 0;
|
|
48
|
+
return (
|
|
49
|
+
<FormItem {...(className !== undefined && { className })}>
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<Checkbox
|
|
52
|
+
id={id}
|
|
53
|
+
checked={!!field.state.value}
|
|
54
|
+
onCheckedChange={(checked) => field.handleChange(checked === true)}
|
|
55
|
+
{...(disabled !== undefined && { disabled })}
|
|
56
|
+
aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
|
|
57
|
+
/>
|
|
58
|
+
<FormLabel htmlFor={id}>{label}</FormLabel>
|
|
59
|
+
</div>
|
|
60
|
+
{description !== undefined ? (
|
|
61
|
+
<FormDescription id={descId}>{description}</FormDescription>
|
|
62
|
+
) : null}
|
|
63
|
+
{hasError ? <FormMessage id={msgId} errors={errors} /> : null}
|
|
64
|
+
</FormItem>
|
|
65
|
+
);
|
|
66
|
+
}}
|
|
67
|
+
</form.Field>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── FormSwitch ──────────────────────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
export interface FormSwitchProps {
|
|
74
|
+
form: AnyFormApi;
|
|
75
|
+
name: string;
|
|
76
|
+
label: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
className?: string;
|
|
80
|
+
validators?: FormFieldValidators;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Switch field integrated with TanStack Form. */
|
|
84
|
+
export function FormSwitch({
|
|
85
|
+
form,
|
|
86
|
+
name,
|
|
87
|
+
label,
|
|
88
|
+
description,
|
|
89
|
+
disabled,
|
|
90
|
+
className,
|
|
91
|
+
validators,
|
|
92
|
+
}: FormSwitchProps) {
|
|
93
|
+
const id = useId();
|
|
94
|
+
const descId = `${id}-desc`;
|
|
95
|
+
const msgId = `${id}-msg`;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<form.Field name={name} {...(validators !== undefined && { validators })}>
|
|
99
|
+
{(field: FieldRenderProps<boolean>) => {
|
|
100
|
+
const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
|
|
101
|
+
const hasError = errors.length > 0;
|
|
102
|
+
return (
|
|
103
|
+
<FormItem {...(className !== undefined && { className })}>
|
|
104
|
+
<div className="flex items-center gap-3">
|
|
105
|
+
<Switch
|
|
106
|
+
id={id}
|
|
107
|
+
checked={!!field.state.value}
|
|
108
|
+
onCheckedChange={(checked) => field.handleChange(checked)}
|
|
109
|
+
{...(disabled !== undefined && { disabled })}
|
|
110
|
+
aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
|
|
111
|
+
/>
|
|
112
|
+
<FormLabel htmlFor={id}>{label}</FormLabel>
|
|
113
|
+
</div>
|
|
114
|
+
{description !== undefined ? (
|
|
115
|
+
<FormDescription id={descId}>{description}</FormDescription>
|
|
116
|
+
) : null}
|
|
117
|
+
{hasError ? <FormMessage id={msgId} errors={errors} /> : null}
|
|
118
|
+
</FormItem>
|
|
119
|
+
);
|
|
120
|
+
}}
|
|
121
|
+
</form.Field>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Form composition helpers: FormFieldArray, FormSubscribe, FormListenEffect,
|
|
5
|
+
* FormActions. These wire common TanStack Form patterns to MiHCM primitives.
|
|
6
|
+
*/
|
|
7
|
+
import { useId, type ReactNode } from 'react';
|
|
8
|
+
import { cn } from '../internal/cn.js';
|
|
9
|
+
import { Button } from '../Button.js';
|
|
10
|
+
import {
|
|
11
|
+
FormDescription,
|
|
12
|
+
FormItem,
|
|
13
|
+
FormLabel,
|
|
14
|
+
} from './building-blocks.js';
|
|
15
|
+
import type { AnyFormApi, FormFieldValidators } from './types.js';
|
|
16
|
+
|
|
17
|
+
/* ── FormFieldArray ──────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
/** Render props supplied to each array item's `renderField` callback. */
|
|
20
|
+
export interface FieldArrayItemApi {
|
|
21
|
+
/** Index of this item within the array. */
|
|
22
|
+
index: number;
|
|
23
|
+
/** Total number of items currently in the array. */
|
|
24
|
+
total: number;
|
|
25
|
+
/** Remove this item from the array. */
|
|
26
|
+
remove: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FormFieldArrayProps {
|
|
30
|
+
form: AnyFormApi;
|
|
31
|
+
name: string;
|
|
32
|
+
label: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
/** Render function called for each array item. */
|
|
35
|
+
renderField: (item: FieldArrayItemApi) => ReactNode;
|
|
36
|
+
/** Label text for the add button. Default: "Add item". */
|
|
37
|
+
addLabel?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
validators?: FormFieldValidators;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Field array integrated with TanStack Form. Provides add/remove buttons. */
|
|
43
|
+
export function FormFieldArray({
|
|
44
|
+
form,
|
|
45
|
+
name,
|
|
46
|
+
label,
|
|
47
|
+
description,
|
|
48
|
+
renderField,
|
|
49
|
+
addLabel = 'Add item',
|
|
50
|
+
className,
|
|
51
|
+
validators,
|
|
52
|
+
}: FormFieldArrayProps) {
|
|
53
|
+
const id = useId();
|
|
54
|
+
const descId = `${id}-desc`;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<form.Field name={name} mode="array" {...(validators !== undefined && { validators })}>
|
|
58
|
+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
59
|
+
{(field: any) => {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
const items = (field.state.value ?? []) as any[];
|
|
62
|
+
return (
|
|
63
|
+
<FormItem {...(className !== undefined && { className })}>
|
|
64
|
+
<div className="flex items-center justify-between">
|
|
65
|
+
<FormLabel>{label}</FormLabel>
|
|
66
|
+
<Button type="button" variant="outline" size="sm" onClick={() => field.pushValue({})}>
|
|
67
|
+
+ {addLabel}
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
{description !== undefined ? (
|
|
71
|
+
<FormDescription id={descId}>{description}</FormDescription>
|
|
72
|
+
) : null}
|
|
73
|
+
<div className="space-y-3">
|
|
74
|
+
{items.map((_: unknown, i: number) => (
|
|
75
|
+
<div
|
|
76
|
+
key={i}
|
|
77
|
+
className="rounded-lg border border-border p-3 transition-all duration-150"
|
|
78
|
+
>
|
|
79
|
+
{renderField({
|
|
80
|
+
index: i,
|
|
81
|
+
total: items.length,
|
|
82
|
+
remove: () => field.removeValue(i),
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</FormItem>
|
|
88
|
+
);
|
|
89
|
+
}}
|
|
90
|
+
</form.Field>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── FormSubscribe ───────────────────────────────────────────────── */
|
|
95
|
+
|
|
96
|
+
export interface FormSubscribeProps<TSelected> {
|
|
97
|
+
form: AnyFormApi;
|
|
98
|
+
/** Selector function that picks values from form state. */
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
selector: (state: any) => TSelected;
|
|
101
|
+
/** Render function receiving the selected state. */
|
|
102
|
+
children: (selected: TSelected) => ReactNode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Convenience wrapper around `form.Subscribe` for reactive state display. */
|
|
106
|
+
export function FormSubscribe<TSelected>({
|
|
107
|
+
form,
|
|
108
|
+
selector,
|
|
109
|
+
children,
|
|
110
|
+
}: FormSubscribeProps<TSelected>) {
|
|
111
|
+
return <form.Subscribe selector={selector}>{children}</form.Subscribe>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ── FormListenEffect ────────────────────────────────────────────── */
|
|
115
|
+
|
|
116
|
+
export interface FormListenEffectProps {
|
|
117
|
+
form: AnyFormApi;
|
|
118
|
+
/** Field name to listen to. */
|
|
119
|
+
name: string;
|
|
120
|
+
/** Listener callbacks (onChange, onBlur, etc.) for side effects. */
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
listeners: Record<string, any>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Invisible field that fires side effects when another field changes.
|
|
127
|
+
* Useful for dependent field updates (e.g. country changes → reset city).
|
|
128
|
+
* Renders nothing visually.
|
|
129
|
+
*/
|
|
130
|
+
export function FormListenEffect({ form, name, listeners }: FormListenEffectProps) {
|
|
131
|
+
return (
|
|
132
|
+
<form.Field name={name} listeners={listeners}>
|
|
133
|
+
{() => null}
|
|
134
|
+
</form.Field>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ── FormActions ─────────────────────────────────────────────────── */
|
|
139
|
+
|
|
140
|
+
export interface FormActionsProps {
|
|
141
|
+
form: AnyFormApi;
|
|
142
|
+
/** Label for the submit button. Default: "Submit". */
|
|
143
|
+
submitLabel?: string;
|
|
144
|
+
/** Label for the reset button. Default: "Reset". */
|
|
145
|
+
resetLabel?: string;
|
|
146
|
+
/** Whether to show the reset button. Default: true. */
|
|
147
|
+
showReset?: boolean;
|
|
148
|
+
/** Disable both buttons externally. */
|
|
149
|
+
disabled?: boolean;
|
|
150
|
+
className?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Footer with submit and reset buttons. Reacts to form submitting state. */
|
|
154
|
+
export function FormActions({
|
|
155
|
+
form,
|
|
156
|
+
submitLabel = 'Submit',
|
|
157
|
+
resetLabel = 'Reset',
|
|
158
|
+
showReset = true,
|
|
159
|
+
disabled,
|
|
160
|
+
className,
|
|
161
|
+
}: FormActionsProps) {
|
|
162
|
+
return (
|
|
163
|
+
<form.Subscribe
|
|
164
|
+
selector={(s: { canSubmit: boolean; isSubmitting: boolean }) => ({
|
|
165
|
+
canSubmit: s.canSubmit,
|
|
166
|
+
isSubmitting: s.isSubmitting,
|
|
167
|
+
})}
|
|
168
|
+
>
|
|
169
|
+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
170
|
+
{(state: any) => (
|
|
171
|
+
<div className={cn('flex items-center gap-3 pt-2', className)}>
|
|
172
|
+
<Button type="submit" disabled={disabled || !state.canSubmit}>
|
|
173
|
+
{state.isSubmitting ? 'Submitting...' : submitLabel}
|
|
174
|
+
</Button>
|
|
175
|
+
{showReset ? (
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
variant="outline"
|
|
179
|
+
disabled={disabled || state.isSubmitting}
|
|
180
|
+
onClick={() => form.reset()}
|
|
181
|
+
>
|
|
182
|
+
{resetLabel}
|
|
183
|
+
</Button>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</form.Subscribe>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Form types.
|
|
3
|
+
*
|
|
4
|
+
* Wraps TanStack React Form's internals at the boundary so the rest of the
|
|
5
|
+
* field components can stay strongly typed without leaking 12-param generics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Validator config passed through to TanStack Form's `form.Field`. */
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export type FormFieldValidators = Record<string, any>;
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
export type AnyFormApi = any;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Field render-prop shape — kept minimal to avoid coupling to TanStack
|
|
17
|
+
* internals that change across minor versions.
|
|
18
|
+
*/
|
|
19
|
+
export interface FieldRenderProps<T> {
|
|
20
|
+
state: {
|
|
21
|
+
value: T;
|
|
22
|
+
meta: { errors: string[]; isTouched: boolean };
|
|
23
|
+
};
|
|
24
|
+
handleChange: (v: T) => void;
|
|
25
|
+
handleBlur: () => void;
|
|
26
|
+
}
|