@axzydev/axzy_ui_system 1.4.7 → 1.5.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.
Files changed (226) hide show
  1. package/README.md +2 -11
  2. package/dist/index.cjs +2174 -1570
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.d.cts +232 -19
  7. package/dist/index.d.ts +232 -19
  8. package/dist/index.js +1838 -1238
  9. package/dist/index.js.map +1 -1
  10. package/dist/layered.css +1 -1
  11. package/package.json +3 -16
  12. package/src/App.tsx +90 -73
  13. package/src/components/atoms/badget/badget.tsx +23 -5
  14. package/src/components/atoms/chip/chip.tsx +3 -1
  15. package/src/components/atoms/image/image.tsx +3 -1
  16. package/src/components/atoms/input/input.tsx +8 -5
  17. package/src/components/atoms/loader/loader.tsx +3 -1
  18. package/src/components/atoms/slide/slide.props.ts +12 -0
  19. package/src/components/atoms/slide/slide.tsx +3 -0
  20. package/src/components/molecules/alert/alert.tsx +3 -1
  21. package/src/components/molecules/breadcrumbs/breadcrumbs.tsx +3 -1
  22. package/src/components/molecules/calendar/calendar.tsx +9 -6
  23. package/src/components/molecules/card/card.tsx +5 -2
  24. package/src/components/molecules/chip-input/chip-input.tsx +9 -7
  25. package/src/components/molecules/date-picker/datePicker.tsx +9 -9
  26. package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +1 -1
  27. package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +6 -3
  28. package/src/components/molecules/field/field.tsx +3 -1
  29. package/src/components/molecules/form-header/form-header.tsx +3 -1
  30. package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +2 -2
  31. package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +20 -15
  32. package/src/components/molecules/masked-input/masked-input.tsx +3 -1
  33. package/src/components/molecules/multi-select/multi-select.tsx +12 -9
  34. package/src/components/molecules/pagination/pagination.tsx +11 -5
  35. package/src/components/molecules/search-select/search-select.tsx +11 -9
  36. package/src/components/molecules/select/select.tsx +6 -4
  37. package/src/components/molecules/stepper/stepper.tsx +6 -4
  38. package/src/components/molecules/table/table.props.ts +10 -2
  39. package/src/components/molecules/table/table.tsx +163 -114
  40. package/src/components/molecules/time-picker/timePicker.tsx +8 -6
  41. package/src/components/molecules/wysiwyg/wysiwyg.tsx +23 -12
  42. package/src/components/organisms/confirm-dialog/confirm-dialog.props.ts +4 -4
  43. package/src/components/organisms/confirm-dialog/confirm-dialog.tsx +16 -8
  44. package/src/components/organisms/data-table/dataTable.props.ts +3 -2
  45. package/src/components/organisms/data-table/dataTable.tsx +149 -81
  46. package/src/components/organisms/dialog/dialog.tsx +3 -0
  47. package/src/components/organisms/drawer/drawer.tsx +3 -0
  48. package/src/components/organisms/dropfile/dropfile.tsx +19 -17
  49. package/src/components/organisms/form-builder/fieldRenderer.tsx +4 -2
  50. package/src/components/organisms/navbar/navbar.tsx +4 -2
  51. package/src/components/organisms/page-header/page-header.tsx +3 -1
  52. package/src/components/organisms/searchTable/components/PaginationControls.tsx +8 -6
  53. package/src/components/organisms/searchTable/components/PaginationInfo.tsx +3 -1
  54. package/src/components/organisms/searchTable/components/SearchAndSortBar.tsx +7 -5
  55. package/src/components/organisms/searchTable/components/SearchInput.tsx +4 -2
  56. package/src/components/organisms/searchTable/components/SortButton.tsx +13 -11
  57. package/src/components/organisms/searchTable/components/TableEmptyState.tsx +4 -2
  58. package/src/components/organisms/searchTable/components/TableRow.tsx +6 -4
  59. package/src/components/organisms/searchTable/searchTable.tsx +4 -2
  60. package/src/components/organisms/sidebar/sidebar.props.ts +1 -1
  61. package/src/components/organisms/sidebar/sidebar.tsx +9 -4
  62. package/src/components/organisms/toast/toast.tsx +3 -1
  63. package/src/components/organisms/topbar/topbar.tsx +4 -2
  64. package/src/components/templates/page/page.props.ts +4 -4
  65. package/src/components/templates/page/page.tsx +6 -4
  66. package/src/components/theme-provider/themeProvider.tsx +28 -17
  67. package/src/dev.css +77 -2
  68. package/src/global.d.ts +10 -0
  69. package/src/i18n/context.ts +9 -0
  70. package/src/i18n/index.ts +22 -0
  71. package/src/i18n/locales/en.ts +163 -0
  72. package/src/i18n/locales/es.ts +165 -0
  73. package/src/i18n/provider.tsx +44 -0
  74. package/src/i18n/translator.ts +82 -0
  75. package/src/i18n/types.ts +54 -0
  76. package/src/i18n/useITTranslation.ts +20 -0
  77. package/src/index.css +0 -1
  78. package/src/index.ts +9 -3
  79. package/src/sandbox/LandingShowcase.tsx +224 -580
  80. package/src/sandbox/NotFoundShowcase.tsx +51 -36
  81. package/src/sandbox/TopbarSearch.tsx +6 -4
  82. package/src/sandbox/controls.tsx +180 -0
  83. package/src/sandbox/i18n.ts +1929 -0
  84. package/src/sandbox/icons.tsx +25 -0
  85. package/src/sandbox/locale.tsx +72 -0
  86. package/src/sandbox/ui.tsx +377 -0
  87. package/src/sandbox/useHashRoute.ts +46 -10
  88. package/src/showcases/DataShowcases.tsx +201 -80
  89. package/src/showcases/FeedbackShowcases.tsx +37 -30
  90. package/src/showcases/FormShowcases.tsx +302 -214
  91. package/src/showcases/GettingStartedShowcase.tsx +151 -145
  92. package/src/showcases/HelpShowcase.tsx +126 -128
  93. package/src/showcases/HomeShowcase.tsx +678 -482
  94. package/src/showcases/LayoutPrimitivesShowcases.tsx +129 -114
  95. package/src/showcases/NavigationShowcases.tsx +45 -37
  96. package/src/showcases/PageShowcases.tsx +171 -152
  97. package/src/showcases/ProductoDetalle.tsx +272 -129
  98. package/src/showcases/ShowcaseLayout.tsx +270 -216
  99. package/src/showcases/SizesShowcase.tsx +123 -64
  100. package/src/showcases/StructureShowcases.tsx +157 -131
  101. package/src/showcases/portfolioUi.tsx +153 -0
  102. package/src/showcases/techIcons.ts +3 -0
  103. package/src/types/table.types.ts +110 -0
  104. package/src/utils/table.utils.ts +10 -2
  105. package/src/components/atoms/avatar/avatar.doc.mdx +0 -48
  106. package/src/components/atoms/avatar/avatar.stories.tsx +0 -64
  107. package/src/components/atoms/badget/badget.doc.mdx +0 -46
  108. package/src/components/atoms/badget/badget.stories.tsx +0 -75
  109. package/src/components/atoms/button/button.doc.mdx +0 -59
  110. package/src/components/atoms/button/button.stories.tsx +0 -173
  111. package/src/components/atoms/checkbox/checkbox.doc.mdx +0 -54
  112. package/src/components/atoms/checkbox/checkbox.stories.tsx +0 -53
  113. package/src/components/atoms/chip/chip.doc.mdx +0 -47
  114. package/src/components/atoms/chip/chip.stories.tsx +0 -64
  115. package/src/components/atoms/divider/divider.doc.mdx +0 -42
  116. package/src/components/atoms/divider/divider.stories.tsx +0 -33
  117. package/src/components/atoms/flex/flex.doc.mdx +0 -59
  118. package/src/components/atoms/flex/flex.stories.tsx +0 -70
  119. package/src/components/atoms/grid/grid.doc.mdx +0 -54
  120. package/src/components/atoms/grid/grid.stories.tsx +0 -71
  121. package/src/components/atoms/image/image.doc.mdx +0 -44
  122. package/src/components/atoms/image/image.stories.tsx +0 -48
  123. package/src/components/atoms/input/input.doc.mdx +0 -70
  124. package/src/components/atoms/input/input.stories.tsx +0 -113
  125. package/src/components/atoms/input-number/input-number.doc.mdx +0 -74
  126. package/src/components/atoms/input-number/input-number.stories.tsx +0 -111
  127. package/src/components/atoms/loader/loader.doc.mdx +0 -43
  128. package/src/components/atoms/loader/loader.stories.tsx +0 -104
  129. package/src/components/atoms/popover/popover.doc.mdx +0 -53
  130. package/src/components/atoms/popover/popover.stories.tsx +0 -24
  131. package/src/components/atoms/progress/progress.doc.mdx +0 -48
  132. package/src/components/atoms/progress/progress.stories.tsx +0 -39
  133. package/src/components/atoms/radio/radio.doc.mdx +0 -62
  134. package/src/components/atoms/radio/radio.stories.tsx +0 -48
  135. package/src/components/atoms/segmented-control/segmented-control.doc.mdx +0 -60
  136. package/src/components/atoms/segmented-control/segmented-control.stories.tsx +0 -62
  137. package/src/components/atoms/skeleton/skeleton.doc.mdx +0 -43
  138. package/src/components/atoms/skeleton/skeleton.stories.tsx +0 -29
  139. package/src/components/atoms/slide/slide.doc.mdx +0 -46
  140. package/src/components/atoms/slide/slide.stories.tsx +0 -120
  141. package/src/components/atoms/slider/slider.doc.mdx +0 -49
  142. package/src/components/atoms/slider/slider.stories.tsx +0 -29
  143. package/src/components/atoms/stack/stack.doc.mdx +0 -126
  144. package/src/components/atoms/stack/stack.stories.tsx +0 -283
  145. package/src/components/atoms/text/text.doc.mdx +0 -41
  146. package/src/components/atoms/text/text.stories.tsx +0 -66
  147. package/src/components/atoms/textarea/textarea.doc.mdx +0 -64
  148. package/src/components/atoms/textarea/textarea.stories.tsx +0 -26
  149. package/src/components/atoms/tooltip/tooltip.doc.mdx +0 -47
  150. package/src/components/atoms/tooltip/tooltip.stories.tsx +0 -19
  151. package/src/components/isolation/isolation.stories.tsx +0 -202
  152. package/src/components/molecules/accordion/accordion.doc.mdx +0 -47
  153. package/src/components/molecules/accordion/accordion.stories.tsx +0 -68
  154. package/src/components/molecules/alert/alert.doc.mdx +0 -48
  155. package/src/components/molecules/alert/alert.stories.tsx +0 -40
  156. package/src/components/molecules/breadcrumbs/breadcrumbs.doc.mdx +0 -57
  157. package/src/components/molecules/breadcrumbs/breadcrumbs.stories.tsx +0 -20
  158. package/src/components/molecules/calendar/calendar.doc.mdx +0 -76
  159. package/src/components/molecules/calendar/calendar.stories.tsx +0 -90
  160. package/src/components/molecules/card/card.doc.mdx +0 -54
  161. package/src/components/molecules/card/card.stories.tsx +0 -57
  162. package/src/components/molecules/chip-input/chip-input.doc.mdx +0 -44
  163. package/src/components/molecules/chip-input/chip-input.stories.tsx +0 -52
  164. package/src/components/molecules/date-picker/date-picker.doc.mdx +0 -65
  165. package/src/components/molecules/date-picker/date-picker.stories.tsx +0 -88
  166. package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +0 -46
  167. package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +0 -41
  168. package/src/components/molecules/empty-state/empty-state.doc.mdx +0 -50
  169. package/src/components/molecules/empty-state/empty-state.stories.tsx +0 -19
  170. package/src/components/molecules/field/field.doc.mdx +0 -43
  171. package/src/components/molecules/field/field.stories.tsx +0 -42
  172. package/src/components/molecules/form-header/form-header.doc.mdx +0 -38
  173. package/src/components/molecules/form-header/form-header.stories.tsx +0 -52
  174. package/src/components/molecules/masked-input/masked-input.doc.mdx +0 -78
  175. package/src/components/molecules/masked-input/masked-input.stories.tsx +0 -126
  176. package/src/components/molecules/multi-select/multi-select.doc.mdx +0 -55
  177. package/src/components/molecules/multi-select/multi-select.stories.tsx +0 -59
  178. package/src/components/molecules/pagination/pagination.doc.mdx +0 -62
  179. package/src/components/molecules/pagination/pagination.stories.tsx +0 -110
  180. package/src/components/molecules/search-select/search-select.doc.mdx +0 -77
  181. package/src/components/molecules/search-select/search-select.stories.tsx +0 -146
  182. package/src/components/molecules/select/select.doc.mdx +0 -75
  183. package/src/components/molecules/select/select.stories.tsx +0 -85
  184. package/src/components/molecules/stat-card/stat-card.doc.mdx +0 -48
  185. package/src/components/molecules/stat-card/stat-card.stories.tsx +0 -40
  186. package/src/components/molecules/stepper/stepper.doc.mdx +0 -54
  187. package/src/components/molecules/stepper/stepper.stories.tsx +0 -154
  188. package/src/components/molecules/table/table.doc.mdx +0 -123
  189. package/src/components/molecules/table/table.stories.tsx +0 -402
  190. package/src/components/molecules/tabs/tabs.doc.mdx +0 -52
  191. package/src/components/molecules/tabs/tabs.stories.tsx +0 -31
  192. package/src/components/molecules/time-picker/timePicker.doc.mdx +0 -68
  193. package/src/components/molecules/time-picker/timePicker.stories.tsx +0 -130
  194. package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +0 -95
  195. package/src/components/organisms/confirm-dialog/confirm-dialog.doc.mdx +0 -60
  196. package/src/components/organisms/confirm-dialog/confirm-dialog.stories.tsx +0 -32
  197. package/src/components/organisms/data-table/ITDataTable.doc.mdx +0 -115
  198. package/src/components/organisms/data-table/ITDataTable.stories.tsx +0 -353
  199. package/src/components/organisms/dialog/dialog.doc.mdx +0 -54
  200. package/src/components/organisms/dialog/dialog.stories.tsx +0 -126
  201. package/src/components/organisms/drawer/drawer.doc.mdx +0 -59
  202. package/src/components/organisms/drawer/drawer.stories.tsx +0 -40
  203. package/src/components/organisms/dropfile/dropfile.doc.mdx +0 -74
  204. package/src/components/organisms/dropfile/dropfile.stories.tsx +0 -93
  205. package/src/components/organisms/form-builder/formBuilder.doc.mdx +0 -251
  206. package/src/components/organisms/form-builder/formBuilder.stories.tsx +0 -914
  207. package/src/components/organisms/navbar/navbar.doc.mdx +0 -77
  208. package/src/components/organisms/navbar/navbar.stories.tsx +0 -83
  209. package/src/components/organisms/page-header/page-header.doc.mdx +0 -54
  210. package/src/components/organisms/page-header/page-header.stories.tsx +0 -74
  211. package/src/components/organisms/searchTable/searchTable.doc.mdx +0 -79
  212. package/src/components/organisms/searchTable/searchTable.stories.tsx +0 -73
  213. package/src/components/organisms/sidebar/sidebar.doc.mdx +0 -176
  214. package/src/components/organisms/sidebar/sidebar.stories.tsx +0 -234
  215. package/src/components/organisms/toast/toast.doc.mdx +0 -50
  216. package/src/components/organisms/toast/toast.stories.tsx +0 -137
  217. package/src/components/organisms/topbar/topbar.doc.mdx +0 -72
  218. package/src/components/organisms/topbar/topbar.stories.tsx +0 -79
  219. package/src/components/organisms/triple-filter/tripleFilter.doc.mdx +0 -62
  220. package/src/components/organisms/triple-filter/tripleFilter.stories.tsx +0 -31
  221. package/src/components/templates/layout/layout.doc.mdx +0 -53
  222. package/src/components/templates/layout/layout.stories.tsx +0 -113
  223. package/src/components/templates/page/page.doc.mdx +0 -86
  224. package/src/components/templates/page/page.stories.tsx +0 -97
  225. package/src/components/theme-provider/themeProvider.doc.mdx +0 -85
  226. package/src/components/theme-provider/themeProvider.stories.tsx +0 -58
@@ -1,914 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
2
- import ITFormBuilder from "./formBuilder";
3
- import { Formik } from "formik";
4
- import * as Yup from "yup";
5
- import { useState } from "react";
6
- import { FieldConfig, FieldConfigV2 } from "@/types/field.types";
7
- import {
8
- FaUser,
9
- FaEnvelope,
10
- FaSearch,
11
- FaLock,
12
- FaCalculator,
13
- FaCommentDots,
14
- FaCheckCircle,
15
- } from "react-icons/fa";
16
- import { expect, fn, userEvent } from "storybook/test";
17
- import { useFormBuilder } from "./useFormBuilder";
18
-
19
- const meta: Meta<typeof ITFormBuilder> = {
20
- title: "Components/Form Elements/ITFormBuilder",
21
- component: ITFormBuilder,
22
- parameters: {
23
- layout: "centered",
24
- docs: {
25
- description: {
26
- component:
27
- "Dynamic form builder using Formik and Yup configuration. Supports Legacy V1 and the powerful Enterprise V2 Grid mapping, validating, calculating, and conditional logic.",
28
- },
29
- },
30
- },
31
- };
32
- export default meta;
33
- type Story = StoryObj<typeof ITFormBuilder>;
34
-
35
- // ─────────────────────────────────────────────────────────────────────────────
36
- // Helpers shared by the test stories
37
- // ─────────────────────────────────────────────────────────────────────────────
38
-
39
- /** Controlled wrapper that drives the builder with local React state. */
40
- const ControlledForm = ({
41
- children,
42
- initialValues,
43
- }: {
44
- children: (props: {
45
- values: Record<string, any>;
46
- setValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
47
- setFieldValue: (field: string, value: any) => Promise<void>;
48
- handleChange: (e: any) => void;
49
- handleBlur: (e: any) => void;
50
- touched: Record<string, boolean>;
51
- errors: Record<string, string>;
52
- }) => React.ReactNode;
53
- initialValues: Record<string, any>;
54
- }) => {
55
- const [values, setValues] = useState<Record<string, any>>(initialValues);
56
- const [touched, setTouched] = useState<Record<string, boolean>>({});
57
- const [errors] = useState<Record<string, string>>({});
58
-
59
- const handleChange = (e: any) => {
60
- const { name, value, type, checked } = e.target ?? {};
61
- setValues((prev) => ({
62
- ...prev,
63
- [name]: type === "checkbox" ? checked : value,
64
- }));
65
- };
66
-
67
- const handleBlur = (e: any) => {
68
- const { name } = e.target ?? {};
69
- if (name) setTouched((prev) => ({ ...prev, [name]: true }));
70
- };
71
-
72
- const setFieldValue = async (field: string, value: any) => {
73
- setValues((prev) => ({ ...prev, [field]: value }));
74
- };
75
-
76
- return (
77
- <>
78
- {children({
79
- values,
80
- setValues,
81
- setFieldValue,
82
- handleChange,
83
- handleBlur,
84
- touched,
85
- errors,
86
- })}
87
- {/* Surface state so test play functions and humans can read it. */}
88
- <pre
89
- data-testid="form-state"
90
- style={{
91
- marginTop: 16,
92
- padding: 8,
93
- background: "#0f172a",
94
- color: "#a7f3d0",
95
- fontSize: 11,
96
- borderRadius: 8,
97
- maxWidth: 480,
98
- overflow: "auto",
99
- }}
100
- >
101
- {JSON.stringify({ values, touched, errors }, null, 2)}
102
- </pre>
103
- </>
104
- );
105
- };
106
-
107
- // ─────────────────────────────────────────────────────────────────────────────
108
- // Showcase stories (kept as-is for the Storybook gallery)
109
- // ─────────────────────────────────────────────────────────────────────────────
110
-
111
- const FormWrapper = ({
112
- children,
113
- initialValues = {},
114
- validationSchema,
115
- widthClass = "w-[800px]",
116
- }: {
117
- children: (props: any) => React.ReactNode;
118
- initialValues?: any;
119
- validationSchema?: any;
120
- widthClass?: string;
121
- }) => {
122
- const [submittedValues, setSubmittedValues] = useState<any>(null);
123
-
124
- return (
125
- <div
126
- className={`p-6 bg-white rounded-xl shadow-sm border border-gray-100 max-w-full ${widthClass}`}
127
- >
128
- <Formik
129
- initialValues={initialValues}
130
- validationSchema={validationSchema}
131
- onSubmit={(values) => {
132
- setSubmittedValues(values);
133
- console.log("Form submitted:", values);
134
- }}
135
- >
136
- {(formikProps) => (
137
- <form onSubmit={formikProps.handleSubmit}>
138
- {children(formikProps)}
139
-
140
- <div className="mt-6 flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
141
- <button
142
- type="button"
143
- className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
144
- onClick={() => formikProps.resetForm()}
145
- >
146
- Limpiar Formulario
147
- </button>
148
- <button
149
- type="submit"
150
- className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 shadow-sm transition-colors"
151
- >
152
- Enviar Datos
153
- </button>
154
- </div>
155
-
156
- {submittedValues && (
157
- <div className="mt-4 p-3 bg-gray-50 rounded-lg text-xs font-mono overflow-auto max-h-40">
158
- <p className="font-semibold text-gray-500 mb-1">
159
- Payload Generado:
160
- </p>
161
- <pre>{JSON.stringify(submittedValues, null, 2)}</pre>
162
- </div>
163
- )}
164
- </form>
165
- )}
166
- </Formik>
167
- </div>
168
- );
169
- };
170
-
171
- const showcaseConfig: FieldConfigV2[] = [
172
- {
173
- name: "username",
174
- label: "Usuario (Icono Izquierdo Fijo)",
175
- type: "text",
176
- placeholder: "Ej. dev123",
177
- required: true,
178
- leftIcon: <FaUser className="text-gray-400" />,
179
- column: { sm: 12, md: 6 },
180
- },
181
- {
182
- name: "promoCode",
183
- label: "Código Promo (Icono Derecho Click)",
184
- type: "text",
185
- placeholder: "Ingresa código...",
186
- rightIcon: (
187
- <button
188
- type="button"
189
- onClick={() => alert("¡Disparando búsqueda de código promocional!")}
190
- className="text-primary-600 hover:text-primary-800 transition px-2"
191
- >
192
- <FaSearch />
193
- </button>
194
- ),
195
- column: { sm: 12, md: 6 },
196
- },
197
- {
198
- name: "email",
199
- label: "Correo (Validación estricta YUP)",
200
- type: "email",
201
- placeholder: "ejemplo@correo.com",
202
- required: true,
203
- leftIcon: <FaEnvelope className="text-gray-400" />,
204
- column: { sm: 12, md: 6 },
205
- },
206
- {
207
- name: "password",
208
- label: "Contraseña",
209
- type: "password",
210
- required: true,
211
- placeholder: "••••••••",
212
- leftIcon: <FaLock className="text-gray-400" />,
213
- column: { sm: 12, md: 6 },
214
- },
215
- {
216
- name: "subtotal",
217
- label: "Subtotal (Dispara OnChange)",
218
- type: "number",
219
- required: true,
220
- currencyFormat: true,
221
- leftIcon: <FaCalculator className="text-gray-400" />,
222
- column: { sm: 12, md: 4 },
223
- onChangeAction: (val, { setFieldValue }) => {
224
- const num = parseFloat(val) || 0;
225
- setFieldValue("iva", (num * 0.16).toFixed(2));
226
- setFieldValue("total", (num * 1.16).toFixed(2));
227
- },
228
- },
229
- {
230
- name: "iva",
231
- label: "IVA (Read-Only)",
232
- type: "number",
233
- currencyFormat: true,
234
- readOnly: true,
235
- column: { sm: 12, md: 4 },
236
- },
237
- {
238
- name: "total",
239
- label: "Total (Disabled)",
240
- type: "number",
241
- currencyFormat: true,
242
- disabled: true,
243
- column: { sm: 12, md: 4 },
244
- },
245
- {
246
- name: "country",
247
- label: "País (Muestra RFC si es MX)",
248
- type: "select",
249
- required: true,
250
- options: [
251
- { value: "", label: "Selecciona país..." },
252
- { value: "MX", label: "México" },
253
- { value: "US", label: "Estados Unidos" },
254
- { value: "OTHER", label: "Otro" },
255
- ],
256
- column: { sm: 12, md: 6 },
257
- },
258
- {
259
- name: "rfc",
260
- label: "RFC de México (Renderización Condicional)",
261
- type: "text",
262
- dependsOn: ["country"],
263
- renderWhen: (vals) => vals.country === "MX",
264
- required: true,
265
- showHintLength: true,
266
- maxLength: 13,
267
- column: { sm: 12, md: 6 },
268
- },
269
- {
270
- name: "date",
271
- label: "Selector de Fecha",
272
- type: "date",
273
- required: true,
274
- column: { sm: 12, md: 4 },
275
- },
276
- {
277
- name: "time",
278
- label: "Selector de Hora",
279
- type: "time" as any,
280
- required: true,
281
- column: { sm: 12, md: 4 },
282
- },
283
- {
284
- name: "comments",
285
- label: "Comentarios (Textarea + Límite)",
286
- type: "textarea" as any,
287
- rows: 3,
288
- showHintLength: true,
289
- maxLength: 200,
290
- leftIcon: <FaCommentDots className="text-gray-400" />,
291
- column: { sm: 12, md: 4 },
292
- },
293
- ];
294
-
295
- const showcaseSchema = Yup.object().shape({
296
- username: Yup.string().required("El nombre de usuario es requerido"),
297
- promoCode: Yup.string(),
298
- email: Yup.string()
299
- .email("Formato de correo inválido")
300
- .required("El correo es requerido"),
301
- password: Yup.string()
302
- .min(8, "Mínimo 8 caracteres")
303
- .required("La contraseña es requerida"),
304
- subtotal: Yup.number()
305
- .typeError("Debe ser un número")
306
- .required("Requerido")
307
- .min(1, "Mayor a 0"),
308
- country: Yup.string().required("Selecciona un país"),
309
- rfc: Yup.string().when("country", {
310
- is: "MX",
311
- then: (schema) =>
312
- schema
313
- .length(13, "Debe tener 13 caracteres exactos")
314
- .required("El RFC es obligatorio para MX"),
315
- otherwise: (schema) => schema.notRequired(),
316
- }),
317
- date: Yup.date().required("Fecha requerida"),
318
- time: Yup.string().required("Hora requerida"),
319
- comments: Yup.string().max(200, "Máximo 200 caracteres"),
320
- });
321
-
322
- export const FullFeaturesShowcaseV2: Story = {
323
- render: () => (
324
- <FormWrapper
325
- initialValues={{
326
- username: "",
327
- promoCode: "",
328
- email: "",
329
- password: "",
330
- subtotal: "",
331
- iva: "",
332
- total: "",
333
- country: "",
334
- rfc: "",
335
- date: null,
336
- time: "",
337
- comments: "",
338
- }}
339
- validationSchema={showcaseSchema}
340
- widthClass="w-[1024px]"
341
- >
342
- {(formikProps) => (
343
- <div className="space-y-4">
344
- <h3 className="text-xl font-bold text-gray-900 mb-2">
345
- ITFormBuilder V2 Completo
346
- </h3>
347
- <p className="text-sm text-gray-500 mb-6">
348
- Prueba los cálculos en tiempo real en "Subtotal", mira el
349
- comportamiento `disabled` vs `readonly`, interactúa con íconos e
350
- ingresa el país "México" para renderizar dinámicamente un campo.
351
- </p>
352
- <ITFormBuilder
353
- config={showcaseConfig as any}
354
- columns={12}
355
- values={formikProps.values}
356
- handleChange={formikProps.handleChange}
357
- handleBlur={formikProps.handleBlur}
358
- touched={formikProps.touched}
359
- errors={formikProps.errors}
360
- setFieldValue={formikProps.setFieldValue}
361
- />
362
- </div>
363
- )}
364
- </FormWrapper>
365
- ),
366
- };
367
-
368
- const loginFields: FieldConfig[] = [
369
- {
370
- name: "email",
371
- label: "Email Address",
372
- type: "text",
373
- placeholder: "john@example.com",
374
- required: true,
375
- column: 12,
376
- },
377
- {
378
- name: "password",
379
- label: "Password",
380
- type: "password",
381
- placeholder: "••••••••",
382
- required: true,
383
- column: 12,
384
- },
385
- ];
386
-
387
- const loginSchema = Yup.object().shape({
388
- email: Yup.string().email("Invalid email").required("Required"),
389
- password: Yup.string().required("Required"),
390
- });
391
-
392
- export const LegacyV1LoginExample: Story = {
393
- render: () => (
394
- <FormWrapper
395
- initialValues={{ email: "", password: "" }}
396
- validationSchema={loginSchema}
397
- widthClass="w-[500px]"
398
- >
399
- {(formikProps) => (
400
- <div className="space-y-4">
401
- <h3 className="text-lg font-semibold text-gray-900 mb-2">
402
- Legacy Login (V1 Config Backward Compatibility)
403
- </h3>
404
- <ITFormBuilder
405
- fields={loginFields}
406
- columns={12}
407
- values={formikProps.values}
408
- handleChange={formikProps.handleChange}
409
- handleBlur={formikProps.handleBlur}
410
- touched={formikProps.touched}
411
- errors={formikProps.errors}
412
- />
413
- </div>
414
- )}
415
- </FormWrapper>
416
- ),
417
- };
418
-
419
- // ─────────────────────────────────────────────────────────────────────────────
420
- // Test stories (interaction tests, run via `pnpm exec vitest run`)
421
- // ─────────────────────────────────────────────────────────────────────────────
422
-
423
- /** Renders a V2 config of three fields and asserts they are present in the DOM. */
424
- export const TestRendersV2Basic: Story = {
425
- name: "Test: renders V2 basic fields",
426
- render: () => (
427
- <ControlledForm initialValues={{ name: "", role: "", dob: null }}>
428
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
429
- <ITFormBuilder
430
- config={[
431
- { name: "name", label: "Name", type: "text", required: true, column: 6 },
432
- {
433
- name: "role",
434
- label: "Role",
435
- type: "select",
436
- required: true,
437
- column: 6,
438
- options: [
439
- { value: "admin", label: "Admin" },
440
- { value: "user", label: "User" },
441
- ],
442
- },
443
- { name: "dob", label: "Date of Birth", type: "date", column: 12 },
444
- ]}
445
- columns={12}
446
- values={values}
447
- handleChange={handleChange}
448
- handleBlur={handleBlur}
449
- touched={touched}
450
- errors={errors}
451
- setFieldValue={setFieldValue}
452
- />
453
- )}
454
- </ControlledForm>
455
- ),
456
- play: async ({ canvas }) => {
457
- await expect(canvas.getByLabelText(/Name/)).toBeInTheDocument();
458
- await expect(canvas.getByLabelText(/Role/)).toBeInTheDocument();
459
- await expect(canvas.getByLabelText(/Date of Birth/)).toBeInTheDocument();
460
- },
461
- };
462
-
463
- /** Typing in the Name input bubbles through `handleChange` into the state. */
464
- export const TestTypingUpdatesState: Story = {
465
- name: "Test: typing updates state via handleChange",
466
- render: () => (
467
- <ControlledForm initialValues={{ email: "" }}>
468
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
469
- <ITFormBuilder
470
- config={[
471
- { name: "email", label: "Email", type: "email", required: true },
472
- ]}
473
- values={values}
474
- handleChange={handleChange}
475
- handleBlur={handleBlur}
476
- touched={touched}
477
- errors={errors}
478
- setFieldValue={setFieldValue}
479
- />
480
- )}
481
- </ControlledForm>
482
- ),
483
- play: async ({ canvas }) => {
484
- const input = canvas.getByLabelText(/Email/);
485
- await userEvent.type(input, "user@example.com");
486
- await expect(input).toHaveValue("user@example.com");
487
-
488
- const stateDump = canvas.getByTestId("form-state");
489
- await expect(stateDump.textContent).toContain('"email": "user@example.com"');
490
- },
491
- };
492
-
493
- /** RFC field appears only when country === "MX"; switching away hides it. */
494
- export const TestConditionalRenderWhen: Story = {
495
- name: "Test: renderWhen shows / hides dependent field",
496
- render: () => (
497
- <ControlledForm initialValues={{ country: "", rfc: "" }}>
498
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
499
- <ITFormBuilder
500
- config={[
501
- {
502
- name: "country",
503
- label: "Country",
504
- type: "select",
505
- required: true,
506
- options: [
507
- { value: "", label: "Select..." },
508
- { value: "MX", label: "México" },
509
- { value: "US", label: "USA" },
510
- ],
511
- },
512
- {
513
- name: "rfc",
514
- label: "RFC",
515
- type: "text",
516
- dependsOn: ["country"],
517
- renderWhen: (v) => v.country === "MX",
518
- required: true,
519
- },
520
- ]}
521
- values={values}
522
- handleChange={handleChange}
523
- handleBlur={handleBlur}
524
- touched={touched}
525
- errors={errors}
526
- setFieldValue={setFieldValue}
527
- />
528
- )}
529
- </ControlledForm>
530
- ),
531
- play: async ({ canvas }) => {
532
- // Initially hidden.
533
- await expect(canvas.queryByLabelText(/RFC/)).toBeNull();
534
-
535
- // Pick Mexico → appears.
536
- const country = canvas.getByLabelText(/Country/);
537
- await userEvent.selectOptions(country, "MX");
538
- await expect(canvas.getByLabelText(/RFC/)).toBeInTheDocument();
539
-
540
- // Switch to USA → hidden again.
541
- await userEvent.selectOptions(country, "US");
542
- await expect(canvas.queryByLabelText(/RFC/)).toBeNull();
543
- },
544
- };
545
-
546
- /** Typing in subtotal triggers `onChangeAction` to recompute total. */
547
- export const TestOnChangeActionComputesTotal: Story = {
548
- name: "Test: onChangeAction updates derived total",
549
- render: () => (
550
- <ControlledForm initialValues={{ subtotal: "", total: "" }}>
551
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
552
- <ITFormBuilder
553
- config={[
554
- {
555
- name: "subtotal",
556
- label: "Subtotal",
557
- type: "number",
558
- currencyFormat: true,
559
- onChangeAction: (val, ctx) => {
560
- const n = parseFloat(String(val).replace(/,/g, "")) || 0;
561
- ctx.setFieldValue("total", (n * 1.16).toFixed(2));
562
- },
563
- },
564
- { name: "total", label: "Total", type: "number", disabled: true },
565
- ]}
566
- values={values}
567
- handleChange={handleChange}
568
- handleBlur={handleBlur}
569
- touched={touched}
570
- errors={errors}
571
- setFieldValue={setFieldValue}
572
- />
573
- )}
574
- </ControlledForm>
575
- ),
576
- play: async ({ canvas }) => {
577
- const subtotal = canvas.getByLabelText(/Subtotal/);
578
- await userEvent.type(subtotal, "100");
579
-
580
- // Total should be derived to "116.50".
581
- const total = canvas.getByLabelText(/Total/) as HTMLInputElement;
582
- await expect(total.value).toBe("116.50");
583
- },
584
- };
585
-
586
- /** Disabled field cannot receive input; programmatic value still renders. */
587
- export const TestDisabledFieldBlocksInput: Story = {
588
- name: "Test: disabled field blocks typing",
589
- render: () => (
590
- <ControlledForm initialValues={{ a: "", b: "fixed" }}>
591
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
592
- <ITFormBuilder
593
- config={[
594
- { name: "a", label: "Active", type: "text" },
595
- { name: "b", label: "Locked", type: "text", disabled: true },
596
- ]}
597
- values={values}
598
- handleChange={handleChange}
599
- handleBlur={handleBlur}
600
- touched={touched}
601
- errors={errors}
602
- setFieldValue={setFieldValue}
603
- />
604
- )}
605
- </ControlledForm>
606
- ),
607
- play: async ({ canvas }) => {
608
- const active = canvas.getByLabelText(/Active/);
609
- const locked = canvas.getByLabelText(/Locked/) as HTMLInputElement;
610
-
611
- await expect(locked).toBeDisabled();
612
- await expect(locked.value).toBe("fixed");
613
-
614
- await userEvent.type(active, "hello");
615
- await userEvent.type(locked, "X"); // should be a no-op
616
- await expect(locked.value).toBe("fixed");
617
- await expect(active).toHaveValue("hello");
618
- },
619
- };
620
-
621
- /** Required flag becomes true once another field exceeds a threshold. */
622
- export const TestDynamicRequired: Story = {
623
- name: "Test: dynamicProps toggles required at runtime",
624
- render: () => (
625
- <ControlledForm initialValues={{ amount: 0, justification: "" }}>
626
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
627
- <ITFormBuilder
628
- config={[
629
- {
630
- name: "amount",
631
- label: "Amount",
632
- type: "number",
633
- defaultValue: 0,
634
- dependsOn: ["amount"],
635
- dynamicProps: (v) =>
636
- Number(v.amount) > 50 ? { required: true } : { required: false },
637
- },
638
- {
639
- name: "justification",
640
- label: "Justification",
641
- type: "text",
642
- dependsOn: ["amount"],
643
- dynamicProps: (v) =>
644
- Number(v.amount) > 50 ? { required: true } : { required: false },
645
- },
646
- ]}
647
- values={values}
648
- handleChange={handleChange}
649
- handleBlur={handleBlur}
650
- touched={touched}
651
- errors={errors}
652
- setFieldValue={setFieldValue}
653
- />
654
- )}
655
- </ControlledForm>
656
- ),
657
- play: async ({ canvas }) => {
658
- const amount = canvas.getByLabelText(/Amount/);
659
- await expect(amount).not.toBeRequired();
660
-
661
- await userEvent.clear(amount);
662
- await userEvent.type(amount, "75");
663
-
664
- const justification = canvas.getByLabelText(/Justification/);
665
- await expect(justification).toBeRequired();
666
- },
667
- };
668
-
669
- /** Async option loader renders a loading placeholder then the resolved list. */
670
- export const TestAsyncOptionsLoad: Story = {
671
- name: "Test: async options loader resolves and renders options",
672
- render: () => {
673
- const config: FieldConfigV2[] = [
674
- {
675
- name: "city",
676
- label: "City",
677
- type: "select",
678
- options: async () => {
679
- await new Promise((r) => setTimeout(r, 50));
680
- return [
681
- { value: "cdmx", label: "Ciudad de México" },
682
- { value: "gdl", label: "Guadalajara" },
683
- ];
684
- },
685
- },
686
- ];
687
- return (
688
- <ControlledForm initialValues={{ city: "" }}>
689
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
690
- <ITFormBuilder
691
- config={config}
692
- values={values}
693
- handleChange={handleChange}
694
- handleBlur={handleBlur}
695
- touched={touched}
696
- errors={errors}
697
- setFieldValue={setFieldValue}
698
- />
699
- )}
700
- </ControlledForm>
701
- );
702
- },
703
- play: async ({ canvas }) => {
704
- // Loading state shown until the loader settles.
705
- await expect(await canvas.findByTestId("field-loading-city")).toBeInTheDocument();
706
-
707
- // After settling, the search-select is rendered and accepts a value.
708
- const select = await canvas.findByLabelText(/City/);
709
- await userEvent.selectOptions(select, "gdl");
710
- await expect(select).toHaveValue("gdl");
711
- },
712
- };
713
-
714
- /** Section type renders the heading and nested fields. */
715
- export const TestSectionRendersNestedFields: Story = {
716
- name: "Test: section renders nested fields",
717
- render: () => (
718
- <ControlledForm initialValues={{ firstName: "", lastName: "" }}>
719
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
720
- <ITFormBuilder
721
- config={[
722
- {
723
- name: "identity",
724
- label: "Personal Information",
725
- type: "section",
726
- fields: [
727
- { name: "firstName", label: "First Name", type: "text" },
728
- { name: "lastName", label: "Last Name", type: "text" },
729
- ],
730
- },
731
- ]}
732
- columns={12}
733
- values={values}
734
- handleChange={handleChange}
735
- handleBlur={handleBlur}
736
- touched={touched}
737
- errors={errors}
738
- setFieldValue={setFieldValue}
739
- />
740
- )}
741
- </ControlledForm>
742
- ),
743
- play: async ({ canvas }) => {
744
- await expect(
745
- canvas.getByRole("heading", { name: /personal information/i }),
746
- ).toBeInTheDocument();
747
- await expect(canvas.getByLabelText(/First Name/)).toBeInTheDocument();
748
- await expect(canvas.getByLabelText(/Last Name/)).toBeInTheDocument();
749
- },
750
- };
751
-
752
- /** Custom `type: "custom"` mounts the user-defined component with the value. */
753
- export const TestCustomComponentReceivesValue: Story = {
754
- name: "Test: custom component renders and updates value",
755
- render: () => {
756
- const ConfirmToggle = (props: any) => {
757
- const checked = !!props.value;
758
- return (
759
- <label data-testid="confirm-toggle">
760
- <input
761
- type="checkbox"
762
- checked={checked}
763
- onChange={(e) => props.onChange?.(e.target.checked)}
764
- />
765
- {props.label}
766
- </label>
767
- );
768
- };
769
- return (
770
- <ControlledForm initialValues={{ confirm: false }}>
771
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
772
- <ITFormBuilder
773
- config={[
774
- {
775
- name: "confirm",
776
- label: "Confirm subscription",
777
- type: "custom",
778
- component: ConfirmToggle,
779
- defaultValue: false,
780
- },
781
- ]}
782
- values={values}
783
- handleChange={handleChange}
784
- handleBlur={handleBlur}
785
- touched={touched}
786
- errors={errors}
787
- setFieldValue={setFieldValue}
788
- />
789
- )}
790
- </ControlledForm>
791
- );
792
- },
793
- play: async ({ canvas }) => {
794
- const checkbox = canvas.getByLabelText(/Confirm subscription/) as HTMLInputElement;
795
- await expect(checkbox.checked).toBe(false);
796
-
797
- await userEvent.click(checkbox);
798
- await expect(checkbox.checked).toBe(true);
799
-
800
- await expect(canvas.getByTestId("form-state").textContent).toContain(
801
- '"confirm": true',
802
- );
803
- },
804
- };
805
-
806
- /** V1 legacy path still renders the supported input types. */
807
- export const TestLegacyV1RendersBasic: Story = {
808
- name: "Test: V1 legacy renders basic inputs",
809
- render: () => (
810
- <ControlledForm initialValues={{ email: "", password: "" }}>
811
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
812
- <ITFormBuilder
813
- fields={[
814
- { name: "email", label: "Email", type: "text", required: true, column: 6 },
815
- { name: "password", label: "Password", type: "password", required: true, column: 6 },
816
- ]}
817
- columns={12}
818
- values={values}
819
- handleChange={handleChange}
820
- handleBlur={handleBlur}
821
- touched={touched}
822
- errors={errors}
823
- setFieldValue={setFieldValue}
824
- />
825
- )}
826
- </ControlledForm>
827
- ),
828
- play: async ({ canvas }) => {
829
- const email = canvas.getByLabelText(/Email/);
830
- const password = canvas.getByLabelText(/Password/);
831
-
832
- await userEvent.type(email, "a@b.co");
833
- await userEvent.type(password, "secret123");
834
-
835
- await expect(email).toHaveValue("a@b.co");
836
- await expect(password).toHaveValue("secret123");
837
- },
838
- };
839
-
840
- /** Custom button can read live progress via `useFormBuilder`. */
841
- export const TestUseFormBuilderProgress: Story = {
842
- name: "Test: useFormBuilder returns live progress",
843
- render: () => {
844
- const ProgressBadge = () => {
845
- const { progress } = useFormBuilder();
846
- return (
847
- <span data-testid="progress-badge">
848
- {progress}% <FaCheckCircle />
849
- </span>
850
- );
851
- };
852
- return (
853
- <ControlledForm initialValues={{ a: "", b: "" }}>
854
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
855
- <ITFormBuilder
856
- config={[
857
- { name: "a", label: "Field A", type: "text", required: true },
858
- { name: "b", label: "Field B", type: "text", required: true },
859
- ]}
860
- values={values}
861
- handleChange={handleChange}
862
- handleBlur={handleBlur}
863
- touched={touched}
864
- errors={errors}
865
- setFieldValue={setFieldValue}
866
- >
867
- <ProgressBadge />
868
- </ITFormBuilder>
869
- )}
870
- </ControlledForm>
871
- );
872
- },
873
- play: async ({ canvas }) => {
874
- const badge = canvas.getByTestId("progress-badge");
875
- await expect(badge.textContent).toContain("0%");
876
-
877
- await userEvent.type(canvas.getByLabelText(/Field A/), "hello");
878
-
879
- await expect(badge.textContent).toContain("50%");
880
-
881
- await userEvent.type(canvas.getByLabelText(/Field B/), "world");
882
-
883
- await expect(badge.textContent).toContain("100%");
884
- },
885
- };
886
-
887
- /** `fn()` is exposed for spy assertions; smoke-validates the props API. */
888
- export const TestPropsApiAcceptsSpies: Story = {
889
- name: "Test: handleChange / setFieldValue spies are called",
890
- render: () => (
891
- <ControlledForm initialValues={{ x: "" }}>
892
- {({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => {
893
- const handleChangeSpy = fn(handleChange);
894
- const setFieldValueSpy = fn(setFieldValue);
895
- return (
896
- <ITFormBuilder
897
- config={[{ name: "x", label: "X", type: "text" }]}
898
- values={values}
899
- handleChange={handleChangeSpy as any}
900
- handleBlur={handleBlur}
901
- touched={touched}
902
- errors={errors}
903
- setFieldValue={setFieldValueSpy}
904
- />
905
- );
906
- }}
907
- </ControlledForm>
908
- ),
909
- play: async ({ canvas }) => {
910
- const input = canvas.getByLabelText(/^X/);
911
- await userEvent.type(input, "z");
912
- await expect(input).toHaveValue("z");
913
- },
914
- };