@axzydev/axzy_ui_system 1.2.16 → 1.3.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 (59) hide show
  1. package/dist/index.cjs +3058 -1407
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +620 -54
  5. package/dist/index.d.ts +620 -54
  6. package/dist/index.js +2974 -1318
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/App.tsx +28 -0
  10. package/src/components/atoms/badget/badget.tsx +2 -4
  11. package/src/components/atoms/chip/chip.doc.mdx +47 -0
  12. package/src/components/atoms/chip/chip.props.ts +32 -0
  13. package/src/components/atoms/chip/chip.stories.tsx +64 -0
  14. package/src/components/atoms/chip/chip.tsx +112 -0
  15. package/src/components/atoms/grid/grid.tsx +1 -2
  16. package/src/components/molecules/accordion/accordion.doc.mdx +47 -0
  17. package/src/components/molecules/accordion/accordion.props.ts +35 -0
  18. package/src/components/molecules/accordion/accordion.stories.tsx +68 -0
  19. package/src/components/molecules/accordion/accordion.tsx +123 -0
  20. package/src/components/molecules/chip-input/chip-input.doc.mdx +44 -0
  21. package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
  22. package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
  23. package/src/components/molecules/chip-input/chip-input.tsx +205 -0
  24. package/src/components/molecules/date-picker/datePicker.tsx +45 -56
  25. package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
  26. package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
  27. package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
  28. package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
  29. package/src/components/molecules/field/field.doc.mdx +43 -0
  30. package/src/components/molecules/field/field.props.ts +23 -0
  31. package/src/components/molecules/field/field.stories.tsx +42 -0
  32. package/src/components/molecules/field/field.tsx +54 -0
  33. package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
  34. package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
  35. package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
  36. package/src/components/molecules/multi-select/multi-select.tsx +386 -0
  37. package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
  38. package/src/components/molecules/search-select/search-select.props.ts +10 -1
  39. package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
  40. package/src/components/molecules/search-select/search-select.tsx +102 -28
  41. package/src/components/molecules/time-picker/timePicker.tsx +14 -26
  42. package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
  43. package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
  44. package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
  45. package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
  46. package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
  47. package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
  48. package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
  49. package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
  50. package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
  51. package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
  52. package/src/hooks/useFloatingPanel.ts +100 -0
  53. package/src/index.ts +24 -4
  54. package/src/showcases/DataShowcases.tsx +76 -0
  55. package/src/showcases/FormShowcases.tsx +396 -10
  56. package/src/showcases/HomeShowcase.tsx +6 -6
  57. package/src/showcases/NavigationShowcases.tsx +72 -2
  58. package/src/showcases/StructureShowcases.tsx +64 -0
  59. package/src/types/chip.types.ts +13 -0
@@ -12,41 +12,108 @@ import { getColSpanClass, getGridColsClass } from "@/utils/styles";
12
12
  import ITText from "@/components/atoms/text/text";
13
13
  import { FaSpinner } from "react-icons/fa";
14
14
 
15
- function useAsyncOptions(options: FieldConfigV2["options"], depends: any[]) {
15
+ /**
16
+ * Resolves `FieldConfigV2["options"]` to a stable `{ value, label }[]` array,
17
+ * calling the function form once per dependency change. Returns a loading flag
18
+ * while an async loader is in flight so the caller can render a placeholder.
19
+ *
20
+ * Sync arrays are returned synchronously; async functions are awaited and the
21
+ * resolved array is memoized until one of the supplied `deps` changes.
22
+ *
23
+ * The `deps` argument is intentionally a dynamic array — callers pass in the
24
+ * values of `dependsOn` siblings so option lists re-fetch when a parent field
25
+ * changes (e.g. cascading country -> city).
26
+ */
27
+ function useAsyncOptions(
28
+ options: FieldConfigV2["options"],
29
+ deps: ReadonlyArray<unknown>,
30
+ ): { options: { value: string; label: string }[]; loading: boolean } {
16
31
  const [resolved, setResolved] = useState<{ value: string; label: string }[]>([]);
17
32
  const [loading, setLoading] = useState(false);
33
+
34
+ // Keep a ref so the async loader closure always reads the freshest options
35
+ // function without re-running the effect on identity changes.
18
36
  const optionsRef = useRef(options);
19
37
  optionsRef.current = options;
20
38
 
21
39
  useEffect(() => {
22
40
  const opts = optionsRef.current;
41
+ let cancelled = false;
42
+
23
43
  if (typeof opts === "function") {
24
44
  setLoading(true);
25
- opts().then((result) => {
26
- setResolved(result.map(o => ({ value: String(o.value), label: o.label })));
27
- setLoading(false);
28
- });
45
+ Promise.resolve()
46
+ .then(() => opts())
47
+ .then((result) => {
48
+ if (cancelled) return;
49
+ setResolved(result.map((o) => ({ value: String(o.value), label: o.label })));
50
+ })
51
+ .catch((err) => {
52
+ if (cancelled) return;
53
+ if (typeof console !== "undefined") {
54
+ console.error("[ITFormBuilder] Failed to load async options:", err);
55
+ }
56
+ setResolved([]);
57
+ })
58
+ .finally(() => {
59
+ if (!cancelled) setLoading(false);
60
+ });
29
61
  } else if (Array.isArray(opts)) {
30
- setResolved(opts.map(o => ({ value: String(o.value), label: o.label })));
62
+ setResolved(opts.map((o) => ({ value: String(o.value), label: o.label })));
63
+ setLoading(false);
64
+ } else {
65
+ setResolved([]);
66
+ setLoading(false);
31
67
  }
32
- // Dependencias dinámicas por diseño (campos dependientes) — no verificables estáticamente.
68
+
69
+ return () => {
70
+ cancelled = true;
71
+ };
72
+ // `deps` is intentionally a dynamic array of dependent-values; the ESLint
73
+ // rule cannot statically verify the dependency surface here.
33
74
  // eslint-disable-next-line react-hooks/exhaustive-deps
34
- }, depends);
75
+ }, deps);
35
76
 
36
77
  return { options: resolved, loading };
37
78
  }
38
79
 
39
-
40
-
41
80
  interface ITFieldRendererProps {
81
+ /** The V2 field definition driving the render. */
42
82
  config: FieldConfigV2;
83
+ /** Grid column count used for `column` spans and nested `section` grids. */
43
84
  columns?: number;
85
+ /** Current value of this field (controlled). */
44
86
  value?: any;
87
+ /** Validation error message for this field. */
45
88
  error?: string;
89
+ /** Whether this field has been touched. */
46
90
  touched?: boolean;
91
+ /**
92
+ * Pre-filtered `values` slice containing only the keys listed in
93
+ * `config.dependsOn`. Computed by {@link ITFormBuilder}.
94
+ */
47
95
  dependentValues?: Record<string, any>;
48
96
  }
49
97
 
98
+ /**
99
+ * Renders a single V2 {@link FieldConfigV2}. Mounts/unmounts the field in the
100
+ * shared registry, evaluates `renderWhen` / `dynamicProps` via
101
+ * {@link useFieldRules}, and dispatches to the correct input component for the
102
+ * field's `type`.
103
+ *
104
+ * Field kinds supported by `type`:
105
+ * - `"text" | "password" | "number" | "email"` -> `<ITInput>`
106
+ * - `"select"` (sync options) -> `<ITSelect>`
107
+ * - `"select"` (async options) -> `<ITSearchSelect>` with a spinner fallback
108
+ * - `"date"` -> `<ITDatePicker>`
109
+ * - `"time"` -> `<ITTimePicker>`
110
+ * - `"custom"` -> the `config.component` renderer
111
+ * - `"section"` -> nested grid of `config.fields`
112
+ * - `"array"` -> placeholder (reserved for future dynamic lists)
113
+ *
114
+ * Memoised with `React.memo`; equality is shallow on props so passing fresh
115
+ * `value`/`error`/`touched` per render is expected.
116
+ */
50
117
  const ITFieldRenderer = ({
51
118
  config,
52
119
  columns = 12,
@@ -66,7 +133,8 @@ const ITFieldRenderer = ({
66
133
  return () => {
67
134
  context.unregisterField(config.name);
68
135
  };
69
- // `config`/`context` son estables en el provider; solo el name define el registro.
136
+ // `config`/`context` are stable in the provider; only `name` defines the
137
+ // registration key.
70
138
  // eslint-disable-next-line react-hooks/exhaustive-deps
71
139
  }, [config.name]);
72
140
 
@@ -94,17 +162,20 @@ const ITFieldRenderer = ({
94
162
  rightIcon,
95
163
  } = activeConfig;
96
164
 
97
- const { options: resolvedOptions, loading: optionsLoading } = useAsyncOptions(options, [options, dependentValues]);
165
+ const { options: resolvedOptions, loading: optionsLoading } = useAsyncOptions(
166
+ options,
167
+ [options, dependentValues],
168
+ );
98
169
 
99
170
  const handleChangeWrapper = useCallback(
100
171
  async (val: any) => {
101
- const finalValue = val?.target ? val.target.value : val;
172
+ const finalValue = val && typeof val === "object" && "target" in val ? val.target.value : val;
102
173
  await context.setFieldValue(name, finalValue);
103
174
  if (activeConfig.onChangeAction) {
104
175
  await activeConfig.onChangeAction(finalValue, context);
105
176
  }
106
177
  },
107
- [name, context, activeConfig]
178
+ [name, context, activeConfig],
108
179
  );
109
180
 
110
181
  if (!isVisible) return null;
@@ -140,15 +211,20 @@ const ITFieldRenderer = ({
140
211
  );
141
212
 
142
213
  case "select":
143
- if (optionsLoading) {
144
- return (
145
- <div className="flex items-center gap-2 py-3">
146
- <FaSpinner className="animate-spin text-primary-500" />
147
- <ITText as="span" className="text-sm text-slate-500">Cargando opciones...</ITText>
148
- </div>
149
- );
150
- }
151
214
  if (typeof options === "function") {
215
+ if (optionsLoading) {
216
+ return (
217
+ <div
218
+ className="flex items-center gap-2 py-3"
219
+ data-testid={`field-loading-${name}`}
220
+ >
221
+ <FaSpinner className="animate-spin text-primary-500" />
222
+ <ITText as="span" className="text-sm text-slate-500">
223
+ Cargando opciones...
224
+ </ITText>
225
+ </div>
226
+ );
227
+ }
152
228
  return (
153
229
  <ITSearchSelect
154
230
  options={resolvedOptions}
@@ -159,7 +235,7 @@ const ITFieldRenderer = ({
159
235
  value={value !== undefined ? value : activeConfig.defaultValue || ""}
160
236
  valueField={valueField || "value"}
161
237
  labelField={labelField || "label"}
162
- onChange={(val: any) => handleChangeWrapper(val)}
238
+ onChange={handleChangeWrapper}
163
239
  onBlur={context.handleBlur}
164
240
  touched={touched}
165
241
  error={error}
@@ -220,9 +296,28 @@ const ITFieldRenderer = ({
220
296
  case "custom":
221
297
  if (activeConfig.component) {
222
298
  const CustomComponent = activeConfig.component;
299
+ // Strip the builder-only keys before forwarding so the custom
300
+ // component receives a clean prop bag (no leaks of `component`,
301
+ // `fields`, `dynamicProps`, `renderWhen`, etc.).
302
+ const customProps: Record<string, unknown> = { ...activeConfig };
303
+ const BUILDER_ONLY_KEYS = [
304
+ "component",
305
+ "type",
306
+ "fields",
307
+ "renderWhen",
308
+ "dynamicProps",
309
+ "dependsOn",
310
+ "validation",
311
+ "asyncValidation",
312
+ "options",
313
+ "defaultValue",
314
+ "className",
315
+ ];
316
+ for (const key of BUILDER_ONLY_KEYS) delete customProps[key];
223
317
  return (
224
318
  <CustomComponent
225
- {...activeConfig}
319
+ {...(customProps as any)}
320
+ name={name}
226
321
  value={value}
227
322
  onChange={handleChangeWrapper}
228
323
  onBlur={context.handleBlur}
@@ -232,17 +327,30 @@ const ITFieldRenderer = ({
232
327
  />
233
328
  );
234
329
  }
330
+ if (typeof console !== "undefined" && process.env.NODE_ENV !== "production") {
331
+ console.warn(
332
+ `[ITFormBuilder] Field "${name}" has type "custom" but no \`component\` was provided.`,
333
+ );
334
+ }
235
335
  return null;
236
336
 
237
337
  case "section":
238
338
  return (
239
339
  <div className={clsx("w-full col-span-full", activeConfig.className)}>
240
340
  {label && (
241
- <ITText as="h4" className="text-lg font-semibold text-secondary-800 mb-4">
341
+ <ITText
342
+ as="h4"
343
+ className="text-lg font-semibold text-secondary-800 mb-4"
344
+ >
242
345
  {label}
243
346
  </ITText>
244
347
  )}
245
- <div className={clsx("grid gap-y-6 gap-x-5", getGridColsClass(columns as any))}>
348
+ <div
349
+ className={clsx(
350
+ "grid gap-y-6 gap-x-5",
351
+ getGridColsClass(columns as any),
352
+ )}
353
+ >
246
354
  {activeConfig.fields?.map((childConfig) => (
247
355
  <ITFieldRenderer
248
356
  key={childConfig.name}
@@ -264,12 +372,21 @@ const ITFieldRenderer = ({
264
372
  );
265
373
 
266
374
  default:
375
+ if (typeof console !== "undefined" && process.env.NODE_ENV !== "production") {
376
+ console.warn(
377
+ `[ITFormBuilder] Unknown V2 field type "${type}" for field "${name}". ` +
378
+ `Supported: text, number, password, email, select, date, time, custom, section, array.`,
379
+ );
380
+ }
267
381
  return null;
268
382
  }
269
383
  };
270
384
 
271
385
  return (
272
- <div className={clsx(getColSpanClass(activeConfig.column, columns), activeConfig.className)}>
386
+ <div
387
+ className={clsx(getColSpanClass(activeConfig.column, columns), activeConfig.className)}
388
+ data-field-name={name}
389
+ >
273
390
  {renderField()}
274
391
  </div>
275
392
  );
@@ -1,30 +1,79 @@
1
1
  import React, { createContext, useContext, useState } from 'react';
2
2
  import { FieldConfigV2, FieldContextV2 } from '@/types/field.types';
3
3
 
4
- interface ITFormBuilderContextType extends FieldContextV2 {
4
+ /**
5
+ * Internal context shared between {@link ITFormBuilder} and any descendant
6
+ * (e.g. a custom submit button, a sidebar showing form progress). The shape
7
+ * extends the public {@link FieldContextV2} with bookkeeping for the V2
8
+ * architecture: a per-field config registry plus lifecycle flags mirroring
9
+ * Formik.
10
+ */
11
+ export interface ITFormBuilderContextType extends FieldContextV2 {
12
+ /** The full V2 config array passed to `<ITFormBuilder config={...} />`. */
5
13
  config: FieldConfigV2[];
14
+ /** True while a Formik `submitForm` is in flight. */
6
15
  isSubmitting: boolean;
16
+ /** True while async validation is running for any field. */
7
17
  isValidating: boolean;
18
+ /** Number of times the parent form has been submitted. */
8
19
  submitCount: number;
20
+ /** Values present at `initialValues` time (reserved for future resets). */
9
21
  initialValues: Record<string, any>;
22
+ /** Validation errors keyed by field `name`. */
10
23
  errors: Record<string, string>;
24
+ /** Touch state keyed by field `name`. */
11
25
  touched: Record<string, boolean>;
12
- handleBlur: (e: any) => void;
26
+ /** Generic change handler (Formik-style `(e) => void`). */
13
27
  handleChange: (e: any) => void;
28
+ /** Generic blur handler (Formik-style `(e) => void`). */
29
+ handleBlur: (e: any) => void;
30
+ /**
31
+ * Registers a field's config in the live registry. Called automatically by
32
+ * every `ITFieldRenderer` on mount; use `getFieldConfig` to read it back.
33
+ */
14
34
  registerField: (name: string, config: FieldConfigV2) => void;
35
+ /** Removes a field from the registry. Called on unmount. */
15
36
  unregisterField: (name: string) => void;
37
+ /** Returns the registered config for a given field name, or `undefined`. */
16
38
  getFieldConfig: (name: string) => FieldConfigV2 | undefined;
39
+ /** Returns the full list of registered field configs (snapshot). */
40
+ getAllFieldConfigs: () => FieldConfigV2[];
17
41
  }
18
42
 
19
43
  const ITFormBuilderContext = createContext<ITFormBuilderContextType | undefined>(undefined);
20
44
 
45
+ export interface ITFormBuilderProviderProps {
46
+ /** The children rendered inside the provider (typically the grid of fields). */
47
+ children: React.ReactNode;
48
+ /**
49
+ * Pre-built value object containing everything except the per-field registry
50
+ * helpers (`registerField` / `unregisterField` / `getFieldConfig` /
51
+ * `getAllFieldConfigs`), which the provider injects itself.
52
+ */
53
+ value: Omit<
54
+ ITFormBuilderContextType,
55
+ 'registerField' | 'unregisterField' | 'getFieldConfig' | 'getAllFieldConfigs'
56
+ >;
57
+ }
58
+
59
+ /**
60
+ * Wraps the V2 grid of fields with the shared context. You normally don't need
61
+ * to mount this manually — {@link ITFormBuilder} does it for you when the V2
62
+ * `config` prop is supplied. Mounting it yourself is useful for advanced
63
+ * compositions (e.g. embedding an `<ITFormBuilder />` inside another component
64
+ * that needs to read `useFormBuilder`).
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * <ITFormBuilderProvider value={{ values, errors, touched, ... }}>
69
+ * <MyGrid />
70
+ * </ITFormBuilderProvider>
71
+ * ```
72
+ */
21
73
  export const ITFormBuilderProvider = ({
22
74
  children,
23
75
  value,
24
- }: {
25
- children: React.ReactNode;
26
- value: Omit<ITFormBuilderContextType, 'registerField' | 'unregisterField' | 'getFieldConfig'>;
27
- }) => {
76
+ }: ITFormBuilderProviderProps) => {
28
77
  const [fieldRegistry, setFieldRegistry] = useState<Record<string, FieldConfigV2>>({});
29
78
 
30
79
  const registerField = React.useCallback((name: string, config: FieldConfigV2) => {
@@ -33,15 +82,21 @@ export const ITFormBuilderProvider = ({
33
82
 
34
83
  const unregisterField = React.useCallback((name: string) => {
35
84
  setFieldRegistry((prev) => {
36
- const newRegistry = { ...prev };
37
- delete newRegistry[name];
38
- return newRegistry;
85
+ const next = { ...prev };
86
+ delete next[name];
87
+ return next;
39
88
  });
40
89
  }, []);
41
90
 
42
- const getFieldConfig = React.useCallback((name: string) => {
43
- return fieldRegistry[name];
44
- }, [fieldRegistry]);
91
+ const getFieldConfig = React.useCallback(
92
+ (name: string) => fieldRegistry[name],
93
+ [fieldRegistry]
94
+ );
95
+
96
+ const getAllFieldConfigs = React.useCallback(
97
+ () => Object.values(fieldRegistry),
98
+ [fieldRegistry]
99
+ );
45
100
 
46
101
  const contextValue = React.useMemo(
47
102
  () => ({
@@ -49,8 +104,9 @@ export const ITFormBuilderProvider = ({
49
104
  registerField,
50
105
  unregisterField,
51
106
  getFieldConfig,
107
+ getAllFieldConfigs,
52
108
  }),
53
- [value, registerField, unregisterField, getFieldConfig]
109
+ [value, registerField, unregisterField, getFieldConfig, getAllFieldConfigs]
54
110
  );
55
111
 
56
112
  return (
@@ -60,7 +116,13 @@ export const ITFormBuilderProvider = ({
60
116
  );
61
117
  };
62
118
 
63
- // Hook de acceso al contexto — patrón requerido junto al provider en el mismo archivo.
119
+ /**
120
+ * Reads the live form context. Throws when called outside an
121
+ * `ITFormBuilderProvider` so misuse fails loudly in development.
122
+ *
123
+ * Prefer the higher-level {@link useFormBuilder} hook for typical use cases
124
+ * (progress, dirty state); this hook returns the un-augmented context shape.
125
+ */
64
126
  // eslint-disable-next-line react-refresh/only-export-components
65
127
  export const useITFormBuilderContext = () => {
66
128
  const context = useContext(ITFormBuilderContext);
@@ -68,4 +130,4 @@ export const useITFormBuilderContext = () => {
68
130
  throw new Error('useITFormBuilderContext must be used within an ITFormBuilderProvider');
69
131
  }
70
132
  return context;
71
- };
133
+ };