@rovula/ui 0.1.7 → 0.1.9

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 (196) hide show
  1. package/dist/cjs/bundle.css +273 -126
  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 +20 -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 +84 -0
  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/Table/Table.js +5 -5
  72. package/dist/components/Tabs/Tabs.js +12 -9
  73. package/dist/components/Tabs/Tabs.stories.js +1 -1
  74. package/dist/components/Text/Text.js +1 -1
  75. package/dist/components/Text/Text.stories.js +1 -1
  76. package/dist/components/TextArea/TextArea.stories.js +1 -1
  77. package/dist/components/TextArea/TextArea.styles.js +3 -3
  78. package/dist/components/TextInput/TextInput.js +3 -2
  79. package/dist/components/TextInput/TextInput.stories.js +3 -3
  80. package/dist/components/TextInput/TextInput.styles.js +41 -19
  81. package/dist/components/Toast/Toast.js +4 -2
  82. package/dist/components/Toast/Toast.stories.js +1 -1
  83. package/dist/components/Toast/Toast.styles.js +4 -4
  84. package/dist/components/Toast/Toaster.js +2 -2
  85. package/dist/components/Tree/Tree.stories.js +1 -1
  86. package/dist/components/Tree/TreeItem.js +1 -1
  87. package/dist/esm/bundle.css +273 -126
  88. package/dist/esm/bundle.js +1545 -1545
  89. package/dist/esm/bundle.js.map +1 -1
  90. package/dist/esm/types/components/AlertDialog/AlertDialog.stories.d.ts +3 -0
  91. package/dist/esm/types/components/Dialog/Dialog.d.ts +7 -1
  92. package/dist/esm/types/components/Dialog/Dialog.stories.d.ts +3 -0
  93. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +2 -0
  94. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +2 -0
  95. package/dist/esm/types/components/Form/Field.d.ts +26 -0
  96. package/dist/esm/types/components/Form/FieldMessage.d.ts +7 -0
  97. package/dist/esm/types/components/Form/Form.d.ts +49 -11
  98. package/dist/esm/types/components/Form/Form.stories.d.ts +23 -0
  99. package/dist/esm/types/components/Form/ValidationHintList.d.ts +20 -0
  100. package/dist/esm/types/components/Form/ValidationHintList.stories.d.ts +9 -0
  101. package/dist/esm/types/components/Form/index.d.ts +10 -0
  102. package/dist/esm/types/components/Form/useOptionBridge.d.ts +17 -0
  103. package/dist/esm/types/components/OtpInput/OtpInput.d.ts +17 -0
  104. package/dist/esm/types/components/OtpInput/OtpInput.stories.d.ts +15 -0
  105. package/dist/esm/types/components/OtpInput/OtpInputGroup.d.ts +25 -0
  106. package/dist/esm/types/components/OtpInput/index.d.ts +5 -0
  107. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +3 -0
  108. package/dist/esm/types/index.d.ts +5 -0
  109. package/dist/esm/types/theme/ThemeColorCoverageRuntime.stories.d.ts +10 -0
  110. package/dist/esm/types/utils/colors.d.ts +84 -0
  111. package/dist/index.d.ts +248 -2
  112. package/dist/index.js +3 -0
  113. package/dist/src/theme/global.css +340 -151
  114. package/dist/theme/ThemeColorCoverageRuntime.stories.js +91 -0
  115. package/dist/utils/colors.js +92 -0
  116. package/package.json +4 -2
  117. package/src/components/ActionButton/ActionButton.stories.tsx +6 -6
  118. package/src/components/ActionButton/ActionButton.styles.ts +1 -1
  119. package/src/components/AlertDialog/AlertDialog.stories.tsx +22 -0
  120. package/src/components/AlertDialog/AlertDialog.tsx +6 -6
  121. package/src/components/Avatar/Avatar.stories.tsx +1 -1
  122. package/src/components/Avatar/Avatar.styles.ts +1 -1
  123. package/src/components/Avatar/AvatarBase.tsx +1 -1
  124. package/src/components/Avatar/AvatarGroup.stories.tsx +1 -1
  125. package/src/components/Button/Buttons.stories.tsx +10 -10
  126. package/src/components/Calendar/Calendar.tsx +3 -3
  127. package/src/components/Checkbox/Checkbox.stories.tsx +35 -12
  128. package/src/components/Checkbox/Checkbox.tsx +7 -5
  129. package/src/components/Collapsible/Collapsible.styles.ts +1 -1
  130. package/src/components/DataTable/DataTable.tsx +2 -2
  131. package/src/components/Dialog/Dialog.stories.tsx +173 -0
  132. package/src/components/Dialog/Dialog.tsx +32 -15
  133. package/src/components/Dropdown/Dropdown.styles.ts +1 -1
  134. package/src/components/Dropdown/Dropdown.tsx +16 -14
  135. package/src/components/DropdownMenu/DropdownMenu.tsx +3 -3
  136. package/src/components/FocusedScrollView/FocusedScrollView.stories.tsx +10 -10
  137. package/src/components/Form/Field.tsx +160 -0
  138. package/src/components/Form/FieldMessage.tsx +38 -0
  139. package/src/components/Form/Form.docs.mdx +67 -0
  140. package/src/components/Form/Form.stories.tsx +490 -0
  141. package/src/components/Form/Form.tsx +185 -87
  142. package/src/components/Form/README.md +284 -0
  143. package/src/components/Form/ValidationHintList.stories.tsx +118 -0
  144. package/src/components/Form/ValidationHintList.tsx +95 -0
  145. package/src/components/Form/index.ts +28 -0
  146. package/src/components/Form/useOptionBridge.ts +55 -0
  147. package/src/components/InputFilter/InputFilter.stories.tsx +1 -1
  148. package/src/components/InputFilter/InputFilter.styles.ts +14 -1
  149. package/src/components/InputFilter/InputFilter.tsx +33 -28
  150. package/src/components/Label/Label.styles.ts +2 -2
  151. package/src/components/Label/Label.tsx +1 -1
  152. package/src/components/Menu/Menu.tsx +12 -12
  153. package/src/components/NumberInput/NumberInput.stories.tsx +1 -1
  154. package/src/components/OtpInput/OtpInput.stories.tsx +168 -0
  155. package/src/components/OtpInput/OtpInput.tsx +223 -0
  156. package/src/components/OtpInput/OtpInputGroup.tsx +74 -0
  157. package/src/components/OtpInput/index.ts +5 -0
  158. package/src/components/PasswordInput/PasswordInput.stories.tsx +1 -1
  159. package/src/components/Popover/Popover.tsx +1 -1
  160. package/src/components/RadioGroup/RadioGroup.stories.tsx +4 -4
  161. package/src/components/RadioGroup/RadioGroup.tsx +2 -1
  162. package/src/components/Search/Search.stories.tsx +1 -1
  163. package/src/components/Search/Search.tsx +6 -2
  164. package/src/components/Slider/Slider.stories.tsx +7 -7
  165. package/src/components/Slider/Slider.tsx +1 -1
  166. package/src/components/Switch/Switch.stories.tsx +4 -4
  167. package/src/components/Table/Table.tsx +5 -5
  168. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  169. package/src/components/Tabs/Tabs.tsx +29 -18
  170. package/src/components/Text/Text.stories.tsx +1 -1
  171. package/src/components/Text/Text.tsx +1 -1
  172. package/src/components/TextArea/TextArea.stories.tsx +1 -1
  173. package/src/components/TextArea/TextArea.styles.ts +3 -3
  174. package/src/components/TextInput/TextInput.stories.tsx +7 -7
  175. package/src/components/TextInput/TextInput.styles.ts +42 -19
  176. package/src/components/TextInput/TextInput.tsx +3 -1
  177. package/src/components/Toast/Toast.stories.tsx +1 -1
  178. package/src/components/Toast/Toast.styles.tsx +7 -7
  179. package/src/components/Toast/Toast.tsx +5 -4
  180. package/src/components/Toast/Toaster.tsx +17 -20
  181. package/src/components/Tree/Tree.stories.tsx +1 -1
  182. package/src/components/Tree/TreeItem.tsx +1 -1
  183. package/src/index.ts +5 -0
  184. package/src/theme/ThemeColorCoverageRuntime.stories.tsx +236 -0
  185. package/src/theme/direct-token-migration-plan.md +121 -0
  186. package/src/theme/figma-mcp-check-report.md +225 -0
  187. package/src/theme/figma-mcp-component-checklist.json +1250 -0
  188. package/src/theme/presets/colors.js +155 -44
  189. package/src/theme/themes/xspector/components/loading.css +2 -2
  190. package/src/theme/tokens/color.css +3 -3
  191. package/src/theme/tokens/components/action-button.css +1 -1
  192. package/src/theme/tokens/components/dropdown-menu.css +3 -3
  193. package/src/theme/tokens/components/loading.css +2 -2
  194. package/src/theme/tokens/components/switch.css +1 -1
  195. package/src/theme/utils.js +164 -25
  196. package/src/utils/colors.ts +92 -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;