@rovula/ui 0.1.6 → 0.1.8

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.
Files changed (232) hide show
  1. package/dist/cjs/bundle.css +630 -467
  2. package/dist/cjs/bundle.js +1545 -1545
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/AlertDialog/AlertDialog.stories.d.ts +3 -0
  5. package/dist/cjs/types/components/Dialog/Dialog.d.ts +7 -1
  6. package/dist/cjs/types/components/Dialog/Dialog.stories.d.ts +3 -0
  7. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +2 -0
  8. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +2 -0
  9. package/dist/cjs/types/components/Form/Field.d.ts +26 -0
  10. package/dist/cjs/types/components/Form/FieldMessage.d.ts +7 -0
  11. package/dist/cjs/types/components/Form/Form.d.ts +49 -11
  12. package/dist/cjs/types/components/Form/Form.stories.d.ts +23 -0
  13. package/dist/cjs/types/components/Form/ValidationHintList.d.ts +17 -0
  14. package/dist/cjs/types/components/Form/ValidationHintList.stories.d.ts +9 -0
  15. package/dist/cjs/types/components/Form/index.d.ts +10 -0
  16. package/dist/cjs/types/components/Form/useOptionBridge.d.ts +17 -0
  17. package/dist/cjs/types/components/OtpInput/OtpInput.d.ts +17 -0
  18. package/dist/cjs/types/components/OtpInput/OtpInput.stories.d.ts +15 -0
  19. package/dist/cjs/types/components/OtpInput/OtpInputGroup.d.ts +25 -0
  20. package/dist/cjs/types/components/OtpInput/index.d.ts +5 -0
  21. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +3 -0
  22. package/dist/cjs/types/index.d.ts +5 -0
  23. package/dist/cjs/types/theme/ThemeColorCoverageRuntime.stories.d.ts +10 -0
  24. package/dist/cjs/types/utils/colors.d.ts +351 -267
  25. package/dist/components/ActionButton/ActionButton.stories.js +2 -2
  26. package/dist/components/ActionButton/ActionButton.styles.js +1 -1
  27. package/dist/components/AlertDialog/AlertDialog.js +6 -6
  28. package/dist/components/AlertDialog/AlertDialog.stories.js +3 -0
  29. package/dist/components/Avatar/Avatar.stories.js +1 -1
  30. package/dist/components/Avatar/Avatar.styles.js +1 -1
  31. package/dist/components/Avatar/AvatarBase.js +1 -1
  32. package/dist/components/Avatar/AvatarGroup.stories.js +1 -1
  33. package/dist/components/Button/Buttons.stories.js +2 -2
  34. package/dist/components/Calendar/Calendar.js +1 -1
  35. package/dist/components/Checkbox/Checkbox.js +1 -1
  36. package/dist/components/Checkbox/Checkbox.stories.js +17 -7
  37. package/dist/components/Collapsible/Collapsible.styles.js +1 -1
  38. package/dist/components/DataTable/DataTable.js +2 -2
  39. package/dist/components/Dialog/Dialog.js +12 -7
  40. package/dist/components/Dialog/Dialog.stories.js +90 -2
  41. package/dist/components/Dropdown/Dropdown.js +2 -2
  42. package/dist/components/DropdownMenu/DropdownMenu.js +3 -3
  43. package/dist/components/FocusedScrollView/FocusedScrollView.stories.js +6 -6
  44. package/dist/components/Form/Field.js +60 -0
  45. package/dist/components/Form/FieldMessage.js +24 -0
  46. package/dist/components/Form/Form.js +73 -41
  47. package/dist/components/Form/Form.stories.js +221 -0
  48. package/dist/components/Form/ValidationHintList.js +30 -0
  49. package/dist/components/Form/ValidationHintList.stories.js +50 -0
  50. package/dist/components/Form/index.js +5 -0
  51. package/dist/components/Form/useOptionBridge.js +27 -0
  52. package/dist/components/InputFilter/InputFilter.js +5 -4
  53. package/dist/components/InputFilter/InputFilter.stories.js +1 -1
  54. package/dist/components/InputFilter/InputFilter.styles.js +14 -1
  55. package/dist/components/Label/Label.styles.js +1 -1
  56. package/dist/components/Menu/Menu.js +2 -2
  57. package/dist/components/NumberInput/NumberInput.stories.js +1 -1
  58. package/dist/components/OtpInput/OtpInput.js +118 -0
  59. package/dist/components/OtpInput/OtpInput.stories.js +60 -0
  60. package/dist/components/OtpInput/OtpInputGroup.js +23 -0
  61. package/dist/components/OtpInput/index.js +3 -0
  62. package/dist/components/PasswordInput/PasswordInput.stories.js +1 -1
  63. package/dist/components/Popover/Popover.js +1 -1
  64. package/dist/components/RadioGroup/RadioGroup.js +1 -1
  65. package/dist/components/RadioGroup/RadioGroup.stories.js +2 -2
  66. package/dist/components/Search/Search.js +13 -1
  67. package/dist/components/Search/Search.stories.js +1 -1
  68. package/dist/components/Slider/Slider.js +1 -1
  69. package/dist/components/Slider/Slider.stories.js +5 -5
  70. package/dist/components/Switch/Switch.stories.js +2 -2
  71. package/dist/components/Switch/Switch.styles.js +1 -1
  72. package/dist/components/Table/Table.js +5 -5
  73. package/dist/components/Tabs/Tabs.js +12 -9
  74. package/dist/components/Tabs/Tabs.stories.js +1 -1
  75. package/dist/components/Text/Text.js +1 -1
  76. package/dist/components/Text/Text.stories.js +1 -1
  77. package/dist/components/TextArea/TextArea.stories.js +1 -1
  78. package/dist/components/TextArea/TextArea.styles.js +3 -3
  79. package/dist/components/TextInput/TextInput.js +3 -2
  80. package/dist/components/TextInput/TextInput.stories.js +3 -3
  81. package/dist/components/TextInput/TextInput.styles.js +41 -19
  82. package/dist/components/Toast/Toast.js +4 -2
  83. package/dist/components/Toast/Toast.stories.js +1 -1
  84. package/dist/components/Toast/Toast.styles.js +4 -4
  85. package/dist/components/Toast/Toaster.js +2 -2
  86. package/dist/components/Tree/Tree.stories.js +1 -1
  87. package/dist/components/Tree/TreeItem.js +1 -1
  88. package/dist/esm/bundle.css +630 -467
  89. package/dist/esm/bundle.js +1545 -1545
  90. package/dist/esm/bundle.js.map +1 -1
  91. package/dist/esm/types/components/AlertDialog/AlertDialog.stories.d.ts +3 -0
  92. package/dist/esm/types/components/Dialog/Dialog.d.ts +7 -1
  93. package/dist/esm/types/components/Dialog/Dialog.stories.d.ts +3 -0
  94. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +2 -0
  95. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +2 -0
  96. package/dist/esm/types/components/Form/Field.d.ts +26 -0
  97. package/dist/esm/types/components/Form/FieldMessage.d.ts +7 -0
  98. package/dist/esm/types/components/Form/Form.d.ts +49 -11
  99. package/dist/esm/types/components/Form/Form.stories.d.ts +23 -0
  100. package/dist/esm/types/components/Form/ValidationHintList.d.ts +17 -0
  101. package/dist/esm/types/components/Form/ValidationHintList.stories.d.ts +9 -0
  102. package/dist/esm/types/components/Form/index.d.ts +10 -0
  103. package/dist/esm/types/components/Form/useOptionBridge.d.ts +17 -0
  104. package/dist/esm/types/components/OtpInput/OtpInput.d.ts +17 -0
  105. package/dist/esm/types/components/OtpInput/OtpInput.stories.d.ts +15 -0
  106. package/dist/esm/types/components/OtpInput/OtpInputGroup.d.ts +25 -0
  107. package/dist/esm/types/components/OtpInput/index.d.ts +5 -0
  108. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +3 -0
  109. package/dist/esm/types/index.d.ts +5 -0
  110. package/dist/esm/types/theme/ThemeColorCoverageRuntime.stories.d.ts +10 -0
  111. package/dist/esm/types/utils/colors.d.ts +351 -267
  112. package/dist/index.d.ts +512 -269
  113. package/dist/index.js +3 -0
  114. package/dist/src/theme/global.css +2739 -2681
  115. package/dist/theme/ThemeColorCoverageRuntime.stories.js +91 -0
  116. package/dist/utils/colors.js +359 -267
  117. package/package.json +4 -2
  118. package/src/components/ActionButton/ActionButton.stories.tsx +6 -6
  119. package/src/components/ActionButton/ActionButton.styles.ts +1 -1
  120. package/src/components/AlertDialog/AlertDialog.stories.tsx +22 -0
  121. package/src/components/AlertDialog/AlertDialog.tsx +6 -6
  122. package/src/components/Avatar/Avatar.stories.tsx +1 -1
  123. package/src/components/Avatar/Avatar.styles.ts +1 -1
  124. package/src/components/Avatar/AvatarBase.tsx +1 -1
  125. package/src/components/Avatar/AvatarGroup.stories.tsx +1 -1
  126. package/src/components/Button/Buttons.stories.tsx +25 -17
  127. package/src/components/Calendar/Calendar.tsx +3 -3
  128. package/src/components/Checkbox/Checkbox.stories.tsx +35 -12
  129. package/src/components/Checkbox/Checkbox.tsx +7 -5
  130. package/src/components/Collapsible/Collapsible.styles.ts +1 -1
  131. package/src/components/DataTable/DataTable.tsx +2 -2
  132. package/src/components/Dialog/Dialog.stories.tsx +173 -0
  133. package/src/components/Dialog/Dialog.tsx +32 -15
  134. package/src/components/Dropdown/Dropdown.styles.ts +1 -1
  135. package/src/components/Dropdown/Dropdown.tsx +16 -14
  136. package/src/components/DropdownMenu/DropdownMenu.tsx +3 -3
  137. package/src/components/FocusedScrollView/FocusedScrollView.stories.tsx +10 -10
  138. package/src/components/Form/Field.tsx +160 -0
  139. package/src/components/Form/FieldMessage.tsx +38 -0
  140. package/src/components/Form/Form.docs.mdx +67 -0
  141. package/src/components/Form/Form.stories.tsx +490 -0
  142. package/src/components/Form/Form.tsx +185 -87
  143. package/src/components/Form/README.md +284 -0
  144. package/src/components/Form/ValidationHintList.stories.tsx +118 -0
  145. package/src/components/Form/ValidationHintList.tsx +82 -0
  146. package/src/components/Form/index.ts +28 -0
  147. package/src/components/Form/useOptionBridge.ts +55 -0
  148. package/src/components/InputFilter/InputFilter.stories.tsx +1 -1
  149. package/src/components/InputFilter/InputFilter.styles.ts +14 -1
  150. package/src/components/InputFilter/InputFilter.tsx +33 -28
  151. package/src/components/Label/Label.styles.ts +2 -2
  152. package/src/components/Label/Label.tsx +1 -1
  153. package/src/components/Menu/Menu.tsx +12 -12
  154. package/src/components/NumberInput/NumberInput.stories.tsx +1 -1
  155. package/src/components/OtpInput/OtpInput.stories.tsx +168 -0
  156. package/src/components/OtpInput/OtpInput.tsx +210 -0
  157. package/src/components/OtpInput/OtpInputGroup.tsx +74 -0
  158. package/src/components/OtpInput/index.ts +5 -0
  159. package/src/components/PasswordInput/PasswordInput.stories.tsx +1 -1
  160. package/src/components/Popover/Popover.tsx +1 -1
  161. package/src/components/RadioGroup/RadioGroup.stories.tsx +4 -4
  162. package/src/components/RadioGroup/RadioGroup.tsx +2 -1
  163. package/src/components/Search/Search.stories.tsx +1 -1
  164. package/src/components/Search/Search.tsx +6 -2
  165. package/src/components/Slider/Slider.stories.tsx +7 -7
  166. package/src/components/Slider/Slider.tsx +1 -1
  167. package/src/components/Switch/Switch.stories.tsx +4 -4
  168. package/src/components/Switch/Switch.styles.ts +1 -1
  169. package/src/components/Table/Table.tsx +5 -5
  170. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  171. package/src/components/Tabs/Tabs.tsx +29 -18
  172. package/src/components/Text/Text.stories.tsx +1 -1
  173. package/src/components/Text/Text.tsx +1 -1
  174. package/src/components/TextArea/TextArea.stories.tsx +1 -1
  175. package/src/components/TextArea/TextArea.styles.ts +3 -3
  176. package/src/components/TextInput/TextInput.stories.tsx +7 -7
  177. package/src/components/TextInput/TextInput.styles.ts +42 -19
  178. package/src/components/TextInput/TextInput.tsx +3 -1
  179. package/src/components/Toast/Toast.stories.tsx +1 -1
  180. package/src/components/Toast/Toast.styles.tsx +7 -7
  181. package/src/components/Toast/Toast.tsx +5 -4
  182. package/src/components/Toast/Toaster.tsx +17 -20
  183. package/src/components/Tree/Tree.stories.tsx +1 -1
  184. package/src/components/Tree/TreeItem.tsx +1 -1
  185. package/src/index.ts +5 -0
  186. package/src/theme/THEME_MAPPING.md +36 -37
  187. package/src/theme/ThemeColorCoverageRuntime.stories.tsx +236 -0
  188. package/src/theme/direct-token-migration-plan.md +121 -0
  189. package/src/theme/figma-mcp-check-report.md +225 -0
  190. package/src/theme/figma-mcp-component-checklist.json +1250 -0
  191. package/src/theme/global.css +7 -3
  192. package/src/theme/presets/colors.js +173 -64
  193. package/src/theme/themes/skyller/baseline.css +0 -4
  194. package/src/theme/themes/variable-mapping.css +1064 -0
  195. package/src/theme/themes/variable.css +248 -230
  196. package/src/theme/themes/xspector/baseline.css +0 -4
  197. package/src/theme/themes/xspector/components/dropdown-menu.css +4 -4
  198. package/src/theme/themes/xspector/components/loading.css +2 -2
  199. package/src/theme/tokens/baseline.css +0 -3
  200. package/src/theme/tokens/color.css +36 -65
  201. package/src/theme/tokens/components/action-button.css +6 -6
  202. package/src/theme/tokens/components/button.css +189 -189
  203. package/src/theme/tokens/components/dropdown-menu.css +5 -5
  204. package/src/theme/tokens/components/footer.css +1 -1
  205. package/src/theme/tokens/components/loading.css +2 -2
  206. package/src/theme/tokens/components/switch.css +11 -11
  207. package/src/theme/tokens/typography.css +28 -28
  208. package/src/theme/tokens_old/baseline.css +13 -0
  209. package/src/theme/tokens_old/color.css +78 -0
  210. package/src/theme/tokens_old/components/action-button.css +127 -0
  211. package/src/theme/tokens_old/components/button.css +512 -0
  212. package/src/theme/tokens_old/components/dropdown-menu.css +27 -0
  213. package/src/theme/tokens_old/components/footer.css +9 -0
  214. package/src/theme/tokens_old/components/loading.css +11 -0
  215. package/src/theme/tokens_old/components/navbar.css +9 -0
  216. package/src/theme/tokens_old/components/progress-bar.css +8 -0
  217. package/src/theme/tokens_old/components/switch.css +29 -0
  218. package/src/theme/tokens_old/typography.css +199 -0
  219. package/src/theme/tokens_old/variables.css +28 -0
  220. package/src/theme/utils.js +172 -33
  221. package/src/utils/colors.ts +367 -278
  222. package/src/theme/themes/skyller/color.css +0 -79
  223. package/src/theme/themes/skyller/palette.css +0 -143
  224. package/src/theme/themes/skyller/state.css +0 -94
  225. package/src/theme/themes/skyller/transparent.css +0 -94
  226. package/src/theme/themes/xspector/color.css +0 -83
  227. package/src/theme/themes/xspector/palette.css +0 -142
  228. package/src/theme/themes/xspector/state.css +0 -94
  229. package/src/theme/themes/xspector/transparent.css +0 -93
  230. /package/src/theme/{tokens → tokens_old}/palette.css +0 -0
  231. /package/src/theme/{tokens → tokens_old}/state.css +0 -0
  232. /package/src/theme/{tokens → tokens_old}/transparent.css +0 -0
@@ -0,0 +1,490 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import type { Resolver } from "react-hook-form";
4
+ import * as yup from "yup";
5
+ import Button from "@/components/Button/Button";
6
+ import TextInput from "@/components/TextInput/TextInput";
7
+ import Dropdown, { Options } from "@/components/Dropdown/Dropdown";
8
+ import DatePicker from "@/components/DatePicker/DatePicker";
9
+ import { NumberInput } from "@/components/NumberInput/NumberInput";
10
+ import { Switch } from "@/components/Switch/Switch";
11
+ import { Field } from "./Field";
12
+ import {
13
+ Form,
14
+ FormController,
15
+ useControlledForm,
16
+ } from "./Form";
17
+
18
+ type LoginFormValues = {
19
+ email: string;
20
+ password: string;
21
+ };
22
+
23
+ type EmployeeFormValues = {
24
+ fullName: string;
25
+ role: string;
26
+ salary?: number;
27
+ startDate?: Date;
28
+ isActive: boolean;
29
+ };
30
+
31
+ type CodeControlFormValues = {
32
+ title: string;
33
+ description: string;
34
+ };
35
+
36
+ const roleOptions: Options[] = [
37
+ { value: "dev", label: "Developer" },
38
+ { value: "pm", label: "Product Manager" },
39
+ { value: "qa", label: "QA Engineer" },
40
+ ];
41
+
42
+ const meta: Meta = {
43
+ title: "Components/Form",
44
+ tags: ["autodocs"],
45
+ parameters: {
46
+ layout: "centered",
47
+ },
48
+ decorators: [
49
+ (Story) => (
50
+ <div className="w-[520px] max-w-full p-5 bg-bg-bg2 rounded-md">
51
+ <Story />
52
+ </div>
53
+ ),
54
+ ],
55
+ };
56
+
57
+ export default meta;
58
+ type Story = StoryObj;
59
+
60
+ const loginSchema = yup.object({
61
+ email: yup
62
+ .string()
63
+ .email("Invalid email format")
64
+ .required("Email is required"),
65
+ password: yup
66
+ .string()
67
+ .required("Password is required")
68
+ .min(6, "Password must be at least 6 characters"),
69
+ });
70
+
71
+ export const BasicYupLogin = {
72
+ args: {},
73
+ render: () => {
74
+ const [lastEmailChange, setLastEmailChange] = useState("");
75
+
76
+ return (
77
+ <Form<LoginFormValues>
78
+ className="flex flex-col gap-3"
79
+ defaultValues={{
80
+ email: "",
81
+ password: "",
82
+ }}
83
+ validationSchema={loginSchema}
84
+ onSubmit={(values: LoginFormValues) => {
85
+ // eslint-disable-next-line no-console
86
+ console.log("Submitted form values:", values);
87
+ }}
88
+ >
89
+ <Field<LoginFormValues, "email">
90
+ name="email"
91
+ component={TextInput}
92
+ componentProps={{
93
+ label: "Email",
94
+ type: "email",
95
+ helperText: "Use your work email",
96
+ required: true,
97
+ }}
98
+ onChange={(value) => {
99
+ setLastEmailChange(String(value ?? ""));
100
+ }}
101
+ />
102
+ <Field<LoginFormValues, "password">
103
+ name="password"
104
+ component={TextInput}
105
+ componentProps={{
106
+ label: "Password",
107
+ type: "password",
108
+ required: true,
109
+ }}
110
+ />
111
+ <div className="text-xs text-text-g-contrast-medium">
112
+ Last email change: {lastEmailChange || "-"}
113
+ </div>
114
+ <Button type="submit">Sign in</Button>
115
+ </Form>
116
+ );
117
+ },
118
+ } satisfies Story;
119
+
120
+ export const MixedUiKitControls = {
121
+ args: {},
122
+ render: () => {
123
+ const selectedRole = useMemo(
124
+ () => roleOptions.find((item) => item.value === "dev"),
125
+ [],
126
+ );
127
+
128
+ return (
129
+ <Form<EmployeeFormValues>
130
+ className="flex flex-col gap-4"
131
+ defaultValues={{
132
+ fullName: "",
133
+ role: selectedRole?.value || "",
134
+ salary: undefined,
135
+ startDate: undefined,
136
+ isActive: true,
137
+ }}
138
+ validationSchema={yup.object({
139
+ fullName: yup.string().required("Full name is required"),
140
+ role: yup.string().required("Please select a role"),
141
+ })}
142
+ onSubmit={(values) => {
143
+ // eslint-disable-next-line no-console
144
+ console.log("Submitted employee values:", values);
145
+ }}
146
+ >
147
+ <Field<EmployeeFormValues, "fullName">
148
+ name="fullName"
149
+ component={TextInput}
150
+ componentProps={{
151
+ label: "Full name",
152
+ required: true,
153
+ }}
154
+ />
155
+
156
+ <Field<
157
+ EmployeeFormValues,
158
+ "role",
159
+ React.ComponentProps<typeof Dropdown>
160
+ >
161
+ name="role"
162
+ component={Dropdown}
163
+ componentProps={{
164
+ label: "Role",
165
+ options: roleOptions,
166
+ required: true,
167
+ helperText: "Choose one role",
168
+ }}
169
+ valuePropName="value"
170
+ changePropName="onSelect"
171
+ parseValue={(incoming) => {
172
+ const option = incoming as Options | undefined;
173
+ return option?.value || "";
174
+ }}
175
+ formatValue={(value) =>
176
+ roleOptions.find((option) => option.value === value)
177
+ }
178
+ />
179
+
180
+ <Field<EmployeeFormValues, "salary">
181
+ name="salary"
182
+ component={NumberInput}
183
+ componentProps={{
184
+ label: "Salary",
185
+ min: 0,
186
+ thousandSeparator: ",",
187
+ formatDisplay: true,
188
+ }}
189
+ parseValue={(incoming) =>
190
+ typeof incoming === "number" ? incoming : undefined
191
+ }
192
+ />
193
+
194
+ <Field<
195
+ EmployeeFormValues,
196
+ "startDate",
197
+ React.ComponentProps<typeof DatePicker>
198
+ >
199
+ name="startDate"
200
+ component={DatePicker}
201
+ componentProps={{
202
+ date: undefined,
203
+ onSelect: () => undefined,
204
+ textInputProps: { label: "Start date", required: false },
205
+ }}
206
+ valuePropName="date"
207
+ changePropName="onSelect"
208
+ blurPropName={false}
209
+ refPropName={false}
210
+ errorMessagePropName={false}
211
+ invalidPropName={false}
212
+ parseValue={(incoming) => incoming as Date | undefined}
213
+ />
214
+
215
+ <div className="flex items-center justify-between rounded-md border border-bg-stroke1 px-3 py-2">
216
+ <span className="text-sm text-text-contrast-max">
217
+ Active employee
218
+ </span>
219
+ <Field<
220
+ EmployeeFormValues,
221
+ "isActive",
222
+ React.ComponentProps<typeof Switch>
223
+ >
224
+ name="isActive"
225
+ component={Switch}
226
+ valuePropName="checked"
227
+ changePropName="onCheckedChange"
228
+ blurPropName={false}
229
+ errorMessagePropName={false}
230
+ invalidPropName={false}
231
+ parseValue={(incoming) => Boolean(incoming)}
232
+ />
233
+ </div>
234
+
235
+ <Button type="submit">Create employee</Button>
236
+ </Form>
237
+ );
238
+ },
239
+ } satisfies Story;
240
+
241
+ export const ResolverBasedValidation = {
242
+ args: {},
243
+ render: () => {
244
+ const resolver: Resolver<LoginFormValues> = async (values) => {
245
+ const errors: {
246
+ email?: { type: string; message: string };
247
+ password?: { type: string; message: string };
248
+ } = {};
249
+
250
+ if (!values.email?.includes("@")) {
251
+ errors.email = { type: "validate", message: "Email must include @" };
252
+ }
253
+ if (!values.password || values.password.length < 8) {
254
+ errors.password = {
255
+ type: "validate",
256
+ message: "Password must be at least 8 characters",
257
+ };
258
+ }
259
+
260
+ return {
261
+ values: Object.keys(errors).length ? {} : values,
262
+ errors,
263
+ };
264
+ };
265
+
266
+ return (
267
+ <Form<LoginFormValues>
268
+ className="flex flex-col gap-3"
269
+ defaultValues={{
270
+ email: "",
271
+ password: "",
272
+ }}
273
+ resolver={resolver}
274
+ onSubmit={(values) => {
275
+ // eslint-disable-next-line no-console
276
+ console.log("Resolver submit values:", values);
277
+ }}
278
+ >
279
+ <Field<LoginFormValues, "email">
280
+ name="email"
281
+ component={TextInput}
282
+ componentProps={{
283
+ label: "Email",
284
+ type: "email",
285
+ required: true,
286
+ }}
287
+ />
288
+ <Field<LoginFormValues, "password">
289
+ name="password"
290
+ component={TextInput}
291
+ componentProps={{
292
+ label: "Password",
293
+ type: "password",
294
+ helperText: "This story demonstrates custom resolver.",
295
+ required: true,
296
+ }}
297
+ />
298
+ <Button type="submit">Validate</Button>
299
+ </Form>
300
+ );
301
+ },
302
+ } satisfies Story;
303
+
304
+ export const RenderPropsCodeControl = {
305
+ args: {},
306
+ render: () => {
307
+ const codeControlSchema = yup.object({
308
+ title: yup.string().required("Title is required"),
309
+ description: yup
310
+ .string()
311
+ .required("Description is required")
312
+ .min(10, "Description must be at least 10 characters"),
313
+ });
314
+
315
+ return (
316
+ <Form<CodeControlFormValues>
317
+ className="flex flex-col gap-3"
318
+ defaultValues={{
319
+ title: "",
320
+ description: "",
321
+ }}
322
+ mode="onChange"
323
+ validationSchema={codeControlSchema}
324
+ onSubmit={(values) => {
325
+ // eslint-disable-next-line no-console
326
+ console.log("Submit with code:", values);
327
+ }}
328
+ >
329
+ {({ formState, getValues, handleSubmit }) => (
330
+ <>
331
+ <Field<CodeControlFormValues, "title">
332
+ name="title"
333
+ component={TextInput}
334
+ componentProps={{
335
+ label: "Title",
336
+ required: true,
337
+ }}
338
+ />
339
+ <Field<CodeControlFormValues, "description">
340
+ name="description"
341
+ component={TextInput}
342
+ componentProps={{
343
+ label: "Description",
344
+ helperText: "Try at least 10 characters",
345
+ required: true,
346
+ }}
347
+ />
348
+
349
+ <div className="flex gap-2">
350
+ <Button
351
+ type="submit"
352
+ disabled={!formState.isValid || formState.isSubmitting}
353
+ isLoading={formState.isSubmitting}
354
+ >
355
+ Submit with code
356
+ </Button>
357
+ </div>
358
+ </>
359
+ )}
360
+ </Form>
361
+ );
362
+ },
363
+ } satisfies Story;
364
+
365
+ export const HigherLayerCodeControl = {
366
+ args: {},
367
+ render: () => {
368
+ const [stateSnapshot, setStateSnapshot] = useState("-");
369
+ const [submitCount, setSubmitCount] = useState(0);
370
+ const formRef = React.useRef<FormController<CodeControlFormValues> | null>(
371
+ null
372
+ );
373
+
374
+ const schema = yup.object({
375
+ title: yup.string().required("Title is required"),
376
+ description: yup
377
+ .string()
378
+ .required("Description is required")
379
+ .min(10, "Description must be at least 10 characters"),
380
+ });
381
+
382
+ const { methods, FormRoot } = useControlledForm<CodeControlFormValues>({
383
+ defaultValues: {
384
+ title: "",
385
+ description: "",
386
+ },
387
+ validationSchema: schema,
388
+ mode: "onChange",
389
+ });
390
+
391
+ const onSubmit = (values: CodeControlFormValues) => {
392
+ setSubmitCount((prev) => prev + 1);
393
+ // eslint-disable-next-line no-console
394
+ console.log("Higher layer submit with code:", values);
395
+ };
396
+
397
+ return (
398
+ <div className="flex flex-col gap-3">
399
+ <div className="rounded-md border border-bg-stroke1 bg-bg-bg2 p-3 text-xs text-text-g-contrast-medium">
400
+ Higher layer state: {methods.formState.isValid ? "valid" : "invalid"}{" "}
401
+ /{methods.formState.isDirty ? " dirty" : " pristine"} / submits:{" "}
402
+ {submitCount}
403
+ </div>
404
+
405
+ <div className="flex gap-2">
406
+ <Button
407
+ type="button"
408
+ variant="outline"
409
+ onClick={() => {
410
+ const snapshot = JSON.stringify({
411
+ values: methods.getValues(),
412
+ isValid: methods.formState.isValid,
413
+ isDirty: methods.formState.isDirty,
414
+ });
415
+ setStateSnapshot(snapshot);
416
+ }}
417
+ >
418
+ Get state with code (outside Form)
419
+ </Button>
420
+ <Button
421
+ type="button"
422
+ disabled={
423
+ !methods.formState.isValid || methods.formState.isSubmitting
424
+ }
425
+ isLoading={methods.formState.isSubmitting}
426
+ onClick={() => {
427
+ methods.handleSubmit(onSubmit)();
428
+ }}
429
+ >
430
+ Submit with code (outside Form)
431
+ </Button>
432
+ <Button
433
+ type="button"
434
+ variant="outline"
435
+ onClick={async () => {
436
+ await formRef.current?.submit();
437
+ }}
438
+ >
439
+ Submit with ref
440
+ </Button>
441
+ <Button
442
+ type="button"
443
+ variant="outline"
444
+ onClick={() => {
445
+ const valuesFromRef = formRef.current?.getValues();
446
+ if (!valuesFromRef) return;
447
+
448
+ setStateSnapshot(
449
+ JSON.stringify({
450
+ values: valuesFromRef,
451
+ via: "controllerRef",
452
+ }),
453
+ );
454
+ }}
455
+ >
456
+ Get state with ref
457
+ </Button>
458
+ </div>
459
+
460
+ <div className="rounded-md border border-bg-stroke1 bg-bg-bg2 p-3 text-xs text-text-g-contrast-medium">
461
+ Snapshot: {stateSnapshot}
462
+ </div>
463
+
464
+ <FormRoot
465
+ ref={formRef}
466
+ className="flex flex-col gap-3"
467
+ onSubmit={onSubmit}
468
+ >
469
+ <Field<CodeControlFormValues, "title">
470
+ name="title"
471
+ component={TextInput}
472
+ componentProps={{
473
+ label: "Title",
474
+ required: true,
475
+ }}
476
+ />
477
+ <Field<CodeControlFormValues, "description">
478
+ name="description"
479
+ component={TextInput}
480
+ componentProps={{
481
+ label: "Description",
482
+ helperText: "Try at least 10 characters",
483
+ required: true,
484
+ }}
485
+ />
486
+ </FormRoot>
487
+ </div>
488
+ );
489
+ },
490
+ } satisfies Story;