@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.
- package/dist/index.cjs +3058 -1407
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +620 -54
- package/dist/index.d.ts +620 -54
- package/dist/index.js +2974 -1318
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +28 -0
- package/src/components/atoms/badget/badget.tsx +2 -4
- package/src/components/atoms/chip/chip.doc.mdx +47 -0
- package/src/components/atoms/chip/chip.props.ts +32 -0
- package/src/components/atoms/chip/chip.stories.tsx +64 -0
- package/src/components/atoms/chip/chip.tsx +112 -0
- package/src/components/atoms/grid/grid.tsx +1 -2
- package/src/components/molecules/accordion/accordion.doc.mdx +47 -0
- package/src/components/molecules/accordion/accordion.props.ts +35 -0
- package/src/components/molecules/accordion/accordion.stories.tsx +68 -0
- package/src/components/molecules/accordion/accordion.tsx +123 -0
- package/src/components/molecules/chip-input/chip-input.doc.mdx +44 -0
- package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
- package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
- package/src/components/molecules/chip-input/chip-input.tsx +205 -0
- package/src/components/molecules/date-picker/datePicker.tsx +45 -56
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
- package/src/components/molecules/field/field.doc.mdx +43 -0
- package/src/components/molecules/field/field.props.ts +23 -0
- package/src/components/molecules/field/field.stories.tsx +42 -0
- package/src/components/molecules/field/field.tsx +54 -0
- package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
- package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
- package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
- package/src/components/molecules/multi-select/multi-select.tsx +386 -0
- package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
- package/src/components/molecules/search-select/search-select.props.ts +10 -1
- package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
- package/src/components/molecules/search-select/search-select.tsx +102 -28
- package/src/components/molecules/time-picker/timePicker.tsx +14 -26
- package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
- package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
- package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
- package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
- package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
- package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
- package/src/hooks/useFloatingPanel.ts +100 -0
- package/src/index.ts +24 -4
- package/src/showcases/DataShowcases.tsx +76 -0
- package/src/showcases/FormShowcases.tsx +396 -10
- package/src/showcases/HomeShowcase.tsx +6 -6
- package/src/showcases/NavigationShowcases.tsx +72 -2
- package/src/showcases/StructureShowcases.tsx +64 -0
- package/src/types/chip.types.ts +13 -0
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
1
2
|
import ITInput from "@/components/atoms/input/input";
|
|
2
3
|
import ITSelect from "@/components/molecules/select/select";
|
|
3
4
|
import { ITFormBuilderProps } from "./formBuilder.props";
|
|
4
5
|
import ITDatePicker from "@/components/molecules/date-picker/datePicker";
|
|
5
|
-
import { useEffect, useState, useMemo } from "react";
|
|
6
6
|
import { ITFormBuilderProvider } from "./formBuilder.context";
|
|
7
7
|
import ITFieldRenderer from "./fieldRenderer";
|
|
8
8
|
import { formGrid, getColSpanClass } from "@/utils/styles";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Builds the `dependentValues` slice that drives a field's `renderWhen` and
|
|
12
|
+
* `dynamicProps` rules. Only the keys listed in `dependsOn` are forwarded, so
|
|
13
|
+
* the field re-renders only when one of its declared dependencies actually
|
|
14
|
+
* changes (memo-friendly).
|
|
15
|
+
*
|
|
16
|
+
* @param dependsOn - Sibling field names declared in {@link FieldConfigV2.dependsOn}.
|
|
17
|
+
* @param values - The full form values object.
|
|
18
|
+
* @returns A filtered object; empty when `dependsOn` is missing or empty.
|
|
19
|
+
*/
|
|
10
20
|
function buildDependentValues(
|
|
11
21
|
dependsOn: string[] | undefined,
|
|
12
22
|
values: Record<string, any>,
|
|
13
|
-
) {
|
|
23
|
+
): Record<string, any> {
|
|
14
24
|
if (!dependsOn || dependsOn.length === 0) return {};
|
|
15
25
|
const deps: Record<string, any> = {};
|
|
16
26
|
for (const key of dependsOn) {
|
|
@@ -20,35 +30,89 @@ function buildDependentValues(
|
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
33
|
+
* Reads the new value out of either a native React change event (`e.target.value`)
|
|
34
|
+
* or an already-extracted payload (Formik's `setFieldValue` call sites do this
|
|
35
|
+
* for custom components).
|
|
36
|
+
*
|
|
37
|
+
* @param arg - Either a React change event or a raw value.
|
|
38
|
+
* @returns The unwrapped string value.
|
|
39
|
+
*/
|
|
40
|
+
function unwrapChangeValue(arg: any): string {
|
|
41
|
+
return arg && typeof arg === "object" && "target" in arg ? arg.target.value : arg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `ITFormBuilder` is a declarative form generator. It renders a responsive
|
|
46
|
+
* grid of inputs from either a legacy V1 `fields` array or the richer V2
|
|
47
|
+
* `config` array and wires every rendered input/select/date to the supplied
|
|
48
|
+
* Formik-style `handleChange`/`handleBlur`/`setFieldValue` callbacks.
|
|
49
|
+
*
|
|
50
|
+
* **V2 features (recommended):**
|
|
51
|
+
* - Rules engine: `renderWhen`, `dynamicProps`, `dependsOn`.
|
|
52
|
+
* - Section grouping with `type: "section"`.
|
|
53
|
+
* - Custom renderers with `type: "custom"`.
|
|
54
|
+
* - Async option loaders via `options: () => Promise<...>`.
|
|
55
|
+
* - Per-field `onChangeAction` for derived fields and side effects.
|
|
56
|
+
* - Nested sections and grids via `fields?: FieldConfigV2[]`.
|
|
27
57
|
*
|
|
28
|
-
*
|
|
58
|
+
* **V1 (legacy):**
|
|
59
|
+
* The `fields` prop accepts a simpler shape (`text` / `number` / `password` /
|
|
60
|
+
* `select` / `date`) without rules, sections, or async options. Kept for
|
|
61
|
+
* backward compatibility.
|
|
62
|
+
*
|
|
63
|
+
* The component is fully controlled: it does not own form state. Wrap it in a
|
|
64
|
+
* Formik `<Formik>` (or any state holder of your choice) and forward its
|
|
65
|
+
* `values`, `handleChange`, `handleBlur`, `touched`, `errors`, and
|
|
66
|
+
* `setFieldValue` props.
|
|
67
|
+
*
|
|
68
|
+
* @example Minimal login form (V1)
|
|
69
|
+
* ```tsx
|
|
29
70
|
* <ITFormBuilder
|
|
30
71
|
* fields={[
|
|
31
|
-
* { name: "email", label: "Email", type: "text", required: true, column:
|
|
32
|
-
* { name: "
|
|
72
|
+
* { name: "email", label: "Email", type: "text", required: true, column: 12 },
|
|
73
|
+
* { name: "password", label: "Password", type: "password", required: true, column: 12 },
|
|
33
74
|
* ]}
|
|
34
|
-
* values={
|
|
35
|
-
* handleChange={handleChange}
|
|
36
|
-
* handleBlur={handleBlur}
|
|
37
|
-
* touched={touched}
|
|
38
|
-
* errors={errors}
|
|
39
|
-
* columns={12}
|
|
75
|
+
* values={formik.values}
|
|
76
|
+
* handleChange={formik.handleChange}
|
|
77
|
+
* handleBlur={formik.handleBlur}
|
|
78
|
+
* touched={formik.touched}
|
|
79
|
+
* errors={formik.errors}
|
|
40
80
|
* />
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example Conditional RFC field with derived total (V2)
|
|
84
|
+
* ```tsx
|
|
85
|
+
* const config: FieldConfigV2[] = [
|
|
86
|
+
* { name: "country", label: "País", type: "select", required: true,
|
|
87
|
+
* options: [{ value: "MX", label: "México" }, { value: "US", label: "USA" }] },
|
|
88
|
+
* { name: "rfc", label: "RFC", type: "text", required: true,
|
|
89
|
+
* dependsOn: ["country"], renderWhen: (v) => v.country === "MX" },
|
|
90
|
+
* { name: "subtotal", label: "Subtotal", type: "number", currencyFormat: true,
|
|
91
|
+
* onChangeAction: (val, ctx) => ctx.setFieldValue("total", (Number(val) * 1.16).toFixed(2)) },
|
|
92
|
+
* { name: "total", label: "Total", type: "number", currencyFormat: true, disabled: true },
|
|
93
|
+
* ];
|
|
41
94
|
*
|
|
42
|
-
* @example
|
|
43
95
|
* <ITFormBuilder
|
|
44
|
-
* config={
|
|
45
|
-
* values={
|
|
46
|
-
* handleChange={handleChange}
|
|
47
|
-
* handleBlur={handleBlur}
|
|
48
|
-
* touched={touched}
|
|
49
|
-
* errors={errors}
|
|
50
|
-
*
|
|
96
|
+
* config={config}
|
|
97
|
+
* values={formik.values}
|
|
98
|
+
* handleChange={formik.handleChange}
|
|
99
|
+
* handleBlur={formik.handleBlur}
|
|
100
|
+
* touched={formik.touched}
|
|
101
|
+
* errors={formik.errors}
|
|
102
|
+
* setFieldValue={formik.setFieldValue}
|
|
51
103
|
* />
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example Reading form progress from a custom submit button
|
|
107
|
+
* ```tsx
|
|
108
|
+
* const SubmitButton = () => {
|
|
109
|
+
* const { progress } = useFormBuilder();
|
|
110
|
+
* return <button disabled={progress < 100}>Submit ({progress}%)</button>;
|
|
111
|
+
* };
|
|
112
|
+
*
|
|
113
|
+
* <ITFormBuilder config={config} values={...} ... />
|
|
114
|
+
* <SubmitButton />
|
|
115
|
+
* ```
|
|
52
116
|
*/
|
|
53
117
|
export default function ITFormBuilder({
|
|
54
118
|
fields,
|
|
@@ -63,29 +127,40 @@ export default function ITFormBuilder({
|
|
|
63
127
|
setFieldTouched = () => Promise.resolve(),
|
|
64
128
|
setFieldError = () => {},
|
|
65
129
|
isSubmitting = false,
|
|
130
|
+
children,
|
|
66
131
|
}: ITFormBuilderProps) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
132
|
+
const providerValue = useMemo(
|
|
133
|
+
() => ({
|
|
134
|
+
config: config || [],
|
|
135
|
+
values: (values as Record<string, any>) || {},
|
|
136
|
+
errors: (errors as Record<string, string>) || {},
|
|
137
|
+
touched: (touched as Record<string, boolean>) || {},
|
|
138
|
+
handleChange,
|
|
139
|
+
handleBlur,
|
|
140
|
+
setFieldValue,
|
|
141
|
+
setFieldTouched,
|
|
142
|
+
setFieldError,
|
|
143
|
+
initialValues: {} as Record<string, any>,
|
|
144
|
+
isSubmitting,
|
|
145
|
+
isValidating: false,
|
|
146
|
+
submitCount: 0,
|
|
147
|
+
}),
|
|
148
|
+
[
|
|
149
|
+
config,
|
|
150
|
+
values,
|
|
151
|
+
errors,
|
|
152
|
+
touched,
|
|
153
|
+
handleChange,
|
|
154
|
+
handleBlur,
|
|
155
|
+
setFieldValue,
|
|
156
|
+
setFieldTouched,
|
|
157
|
+
setFieldError,
|
|
158
|
+
isSubmitting,
|
|
159
|
+
],
|
|
160
|
+
);
|
|
88
161
|
|
|
162
|
+
// V2 path: render inside the context provider so custom children (sections,
|
|
163
|
+
// custom renderers, useFormBuilder() consumers) can read the registry.
|
|
89
164
|
if (config) {
|
|
90
165
|
return (
|
|
91
166
|
<ITFormBuilderProvider value={providerValue}>
|
|
@@ -95,17 +170,20 @@ export default function ITFormBuilder({
|
|
|
95
170
|
key={fieldConfig.name}
|
|
96
171
|
config={fieldConfig}
|
|
97
172
|
columns={columns}
|
|
98
|
-
value={values[fieldConfig.name]}
|
|
99
|
-
error={errors[fieldConfig.name]}
|
|
100
|
-
touched={touched[fieldConfig.name]}
|
|
101
|
-
dependentValues={buildDependentValues(fieldConfig.dependsOn, values)}
|
|
173
|
+
value={(values as Record<string, any>)[fieldConfig.name]}
|
|
174
|
+
error={(errors as Record<string, string>)[fieldConfig.name]}
|
|
175
|
+
touched={(touched as Record<string, boolean>)[fieldConfig.name]}
|
|
176
|
+
dependentValues={buildDependentValues(fieldConfig.dependsOn, values as Record<string, any>)}
|
|
102
177
|
/>
|
|
103
178
|
))}
|
|
104
179
|
</div>
|
|
180
|
+
{children}
|
|
105
181
|
</ITFormBuilderProvider>
|
|
106
182
|
);
|
|
107
183
|
}
|
|
108
184
|
|
|
185
|
+
// V1 path: legacy inline renderer. Kept for backward compatibility with the
|
|
186
|
+
// original `fields` API. No context is provided.
|
|
109
187
|
return (
|
|
110
188
|
<div className={formGrid(columns)}>
|
|
111
189
|
{fields?.map(({
|
|
@@ -137,17 +215,17 @@ export default function ITFormBuilder({
|
|
|
137
215
|
label={label}
|
|
138
216
|
placeholder={placeholder}
|
|
139
217
|
disabled={disabled}
|
|
140
|
-
value={values[name]}
|
|
218
|
+
value={(values as Record<string, any>)[name]}
|
|
141
219
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
142
220
|
handleChange(e);
|
|
143
|
-
if (onChangeAction
|
|
144
|
-
onChangeAction(e
|
|
221
|
+
if (onChangeAction) {
|
|
222
|
+
onChangeAction(unwrapChangeValue(e), setFieldValue);
|
|
145
223
|
}
|
|
146
224
|
}}
|
|
147
225
|
onBlur={handleBlur}
|
|
148
226
|
currencyFormat={props.currencyFormat}
|
|
149
|
-
touched={touched[name]}
|
|
150
|
-
error={errors[name]}
|
|
227
|
+
touched={(touched as Record<string, boolean>)[name]}
|
|
228
|
+
error={(errors as Record<string, string>)[name]}
|
|
151
229
|
required={required}
|
|
152
230
|
iconRight={props.rightIcon}
|
|
153
231
|
iconLeft={props.leftIcon}
|
|
@@ -166,18 +244,18 @@ export default function ITFormBuilder({
|
|
|
166
244
|
disabled={disabled}
|
|
167
245
|
label={label}
|
|
168
246
|
placeholder={placeholder}
|
|
169
|
-
value={values[name]}
|
|
247
|
+
value={(values as Record<string, any>)[name]}
|
|
170
248
|
valueField={valueField}
|
|
171
249
|
labelField={labelField}
|
|
172
250
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
173
251
|
handleChange(e);
|
|
174
|
-
if (onChangeAction
|
|
175
|
-
onChangeAction(e
|
|
252
|
+
if (onChangeAction) {
|
|
253
|
+
onChangeAction(unwrapChangeValue(e), setFieldValue);
|
|
176
254
|
}
|
|
177
255
|
}}
|
|
178
256
|
onBlur={handleBlur}
|
|
179
|
-
touched={touched[name]}
|
|
180
|
-
error={errors[name]}
|
|
257
|
+
touched={(touched as Record<string, boolean>)[name]}
|
|
258
|
+
error={(errors as Record<string, string>)[name]}
|
|
181
259
|
required={required}
|
|
182
260
|
/>
|
|
183
261
|
);
|
|
@@ -187,25 +265,30 @@ export default function ITFormBuilder({
|
|
|
187
265
|
name={name}
|
|
188
266
|
disabled={disabled}
|
|
189
267
|
label={label}
|
|
190
|
-
value={values[name]}
|
|
191
|
-
onChange={(
|
|
192
|
-
e:
|
|
193
|
-
| React.ChangeEvent<HTMLInputElement>
|
|
194
|
-
| { target: { name: string; value: Date } }
|
|
195
|
-
) => {
|
|
268
|
+
value={(values as Record<string, any>)[name]}
|
|
269
|
+
onChange={(e: any) => {
|
|
196
270
|
handleChange(e);
|
|
197
|
-
if (onChangeAction
|
|
198
|
-
onChangeAction(e
|
|
271
|
+
if (onChangeAction) {
|
|
272
|
+
onChangeAction(unwrapChangeValue(e), setFieldValue);
|
|
199
273
|
}
|
|
200
274
|
}}
|
|
201
275
|
placeholder={placeholder}
|
|
202
276
|
onBlur={handleBlur}
|
|
203
|
-
touched={touched[name]}
|
|
204
|
-
error={errors[name]}
|
|
277
|
+
touched={(touched as Record<string, boolean>)[name]}
|
|
278
|
+
error={(errors as Record<string, string>)[name]}
|
|
205
279
|
required={required}
|
|
206
280
|
/>
|
|
207
281
|
);
|
|
208
282
|
default:
|
|
283
|
+
// V1 has no implicit fallback: unknown types are skipped so a
|
|
284
|
+
// typo or unsupported `type` does not render a broken input.
|
|
285
|
+
if (typeof console !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
286
|
+
console.warn(
|
|
287
|
+
`[ITFormBuilder] Unknown legacy V1 field type "${type}" for field "${name}". ` +
|
|
288
|
+
`Supported V1 types: text, number, password, select, date. ` +
|
|
289
|
+
`For richer features switch to the V2 \`config\` prop.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
209
292
|
return null;
|
|
210
293
|
}
|
|
211
294
|
})()}
|
|
@@ -213,4 +296,4 @@ export default function ITFormBuilder({
|
|
|
213
296
|
))}
|
|
214
297
|
</div>
|
|
215
298
|
);
|
|
216
|
-
}
|
|
299
|
+
}
|
|
@@ -2,46 +2,84 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import { useITFormBuilderContext } from './formBuilder.context';
|
|
3
3
|
import type { FieldConfigV2 } from '@/types/field.types';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Public hook for accessing the live form state from inside (or alongside) an
|
|
7
|
+
* {@link ITFormBuilder}. Must be used by a descendant of `ITFormBuilderProvider`
|
|
8
|
+
* (which is mounted automatically when `ITFormBuilder` receives the V2 `config`
|
|
9
|
+
* prop).
|
|
10
|
+
*
|
|
11
|
+
* Returns the full form context plus two derived values:
|
|
12
|
+
* - `progress`: 0..100 percentage of required fields that are filled and have
|
|
13
|
+
* no error. Returns `100` when no required fields are registered or when the
|
|
14
|
+
* registry is empty.
|
|
15
|
+
* - `isDirty`: `true` when at least one field has been touched.
|
|
16
|
+
*
|
|
17
|
+
* @example Reading progress and triggering a submit
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const SubmitButton = () => {
|
|
20
|
+
* const { progress, isSubmitting } = useFormBuilder();
|
|
21
|
+
* return (
|
|
22
|
+
* <button disabled={progress < 100 || isSubmitting}>
|
|
23
|
+
* {isSubmitting ? "Saving..." : `Submit (${progress}%)`}
|
|
24
|
+
* </button>
|
|
25
|
+
* );
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
5
29
|
export const useFormBuilder = () => {
|
|
6
30
|
const context = useITFormBuilderContext();
|
|
7
|
-
const { values, errors, touched } = context;
|
|
31
|
+
const { values, errors, touched, getAllFieldConfigs } = context;
|
|
8
32
|
|
|
9
33
|
const progress = useMemo(() => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
const allConfigs = getAllFieldConfigs();
|
|
35
|
+
|
|
36
|
+
const requiredFields = allConfigs.filter((cfg) => {
|
|
37
|
+
if (typeof cfg.required === 'function') {
|
|
38
|
+
return cfg.required(values);
|
|
39
|
+
}
|
|
40
|
+
return !!cfg.required;
|
|
17
41
|
});
|
|
18
42
|
|
|
19
43
|
if (requiredFields.length === 0) return 100;
|
|
20
44
|
|
|
21
45
|
let filledFields = 0;
|
|
22
|
-
requiredFields
|
|
23
|
-
const val = values[
|
|
46
|
+
for (const cfg of requiredFields) {
|
|
47
|
+
const val = values[cfg.name];
|
|
24
48
|
const hasValue = val !== undefined && val !== null && val !== '';
|
|
25
|
-
const hasError = !!errors[
|
|
49
|
+
const hasError = !!errors[cfg.name];
|
|
26
50
|
if (hasValue && !hasError) filledFields++;
|
|
27
|
-
}
|
|
51
|
+
}
|
|
28
52
|
|
|
29
53
|
return Math.round((filledFields / requiredFields.length) * 100);
|
|
30
|
-
}, [values, errors,
|
|
54
|
+
}, [values, errors, getAllFieldConfigs]);
|
|
55
|
+
|
|
56
|
+
const isDirty = useMemo(
|
|
57
|
+
() => Object.values(touched).some(Boolean),
|
|
58
|
+
[touched]
|
|
59
|
+
);
|
|
31
60
|
|
|
32
61
|
return {
|
|
33
62
|
...context,
|
|
34
63
|
progress,
|
|
35
|
-
isDirty
|
|
64
|
+
isDirty,
|
|
36
65
|
};
|
|
37
66
|
};
|
|
38
67
|
|
|
39
68
|
/**
|
|
40
|
-
* Hook
|
|
41
|
-
*
|
|
69
|
+
* Hook that evaluates the dynamic rules of a single field given the values of
|
|
70
|
+
* its declared `dependsOn` siblings. Designed to be cheap: it does NOT subscribe
|
|
71
|
+
* to the entire `values` map, only to the subset passed as `dependentValues`,
|
|
72
|
+
* which enables `React.memo` isolation across siblings.
|
|
42
73
|
*
|
|
43
|
-
* `dependentValues` should be
|
|
44
|
-
*
|
|
74
|
+
* `dependentValues` should be the filtered slice of `values` containing only
|
|
75
|
+
* the keys listed in {@link FieldConfigV2.dependsOn}. The {@link ITFormBuilder}
|
|
76
|
+
* computes this slice per-field automatically.
|
|
77
|
+
*
|
|
78
|
+
* Returned shape:
|
|
79
|
+
* - `isVisible`: whether the field should render at all (controls `renderWhen`).
|
|
80
|
+
* - `dynamicProps`: partial override produced by `dynamicProps(dependentValues)`.
|
|
81
|
+
* - `isRequired`: resolved `required` (dynamic > function > boolean).
|
|
82
|
+
* - `isDisabled`: resolved `disabled` (dynamic > function > boolean).
|
|
45
83
|
*/
|
|
46
84
|
export const useFieldRules = (
|
|
47
85
|
config: FieldConfigV2,
|
|
@@ -65,7 +103,7 @@ export const useFieldRules = (
|
|
|
65
103
|
if (typeof fieldConfig.required === 'function') {
|
|
66
104
|
return fieldConfig.required(dependentValues);
|
|
67
105
|
}
|
|
68
|
-
return fieldConfig.required
|
|
106
|
+
return !!fieldConfig.required;
|
|
69
107
|
}, [fieldConfig, dynamicProps.required, dependentValues]);
|
|
70
108
|
|
|
71
109
|
const isDisabled = useMemo(() => {
|
|
@@ -73,8 +111,8 @@ export const useFieldRules = (
|
|
|
73
111
|
if (typeof fieldConfig.disabled === 'function') {
|
|
74
112
|
return fieldConfig.disabled(dependentValues);
|
|
75
113
|
}
|
|
76
|
-
return fieldConfig.disabled
|
|
114
|
+
return !!fieldConfig.disabled;
|
|
77
115
|
}, [fieldConfig, dynamicProps.disabled, dependentValues]);
|
|
78
116
|
|
|
79
117
|
return { isVisible, dynamicProps, isRequired, isDisabled };
|
|
80
|
-
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CSSProperties, RefObject, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** Options for {@link useFloatingPanel}. */
|
|
4
|
+
export interface UseFloatingPanelOptions {
|
|
5
|
+
/** Fallback panel height in px, used before the panel is measured. */
|
|
6
|
+
estimatedHeight?: number;
|
|
7
|
+
/** Gap in px between the anchor and the panel. */
|
|
8
|
+
offset?: number;
|
|
9
|
+
/** When true, the panel width matches the anchor width. */
|
|
10
|
+
matchWidth?: boolean;
|
|
11
|
+
/** Horizontal alignment relative to the anchor. @default "start" */
|
|
12
|
+
align?: "start" | "end";
|
|
13
|
+
/** Panel z-index. */
|
|
14
|
+
zIndex?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Computes a fixed position for a floating panel anchored to an element.
|
|
19
|
+
*
|
|
20
|
+
* Positions are relative to the viewport, so the panel must be rendered
|
|
21
|
+
* through a portal (e.g. into `document.body`) to escape ancestors with
|
|
22
|
+
* `overflow: hidden` or `transform`, which otherwise clip or re-anchor it.
|
|
23
|
+
* When there is not enough room below, the panel flips above and its bottom
|
|
24
|
+
* edge is pinned to the anchor's top, so it always stays adjacent regardless
|
|
25
|
+
* of its real height. Repositions on scroll, resize, and panel resize.
|
|
26
|
+
*
|
|
27
|
+
* @param anchorRef - Ref to the element the panel is anchored to.
|
|
28
|
+
* @param isOpen - Whether the panel is currently open.
|
|
29
|
+
* @param options - Sizing and placement options.
|
|
30
|
+
* @returns `panelRef` to attach to the portaled panel and the `style` to apply.
|
|
31
|
+
*/
|
|
32
|
+
export function useFloatingPanel(
|
|
33
|
+
anchorRef: RefObject<HTMLElement | null>,
|
|
34
|
+
isOpen: boolean,
|
|
35
|
+
{
|
|
36
|
+
estimatedHeight = 300,
|
|
37
|
+
offset = 4,
|
|
38
|
+
matchWidth = true,
|
|
39
|
+
align = "start",
|
|
40
|
+
zIndex = 70,
|
|
41
|
+
}: UseFloatingPanelOptions = {}
|
|
42
|
+
) {
|
|
43
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const [style, setStyle] = useState<CSSProperties>({});
|
|
45
|
+
|
|
46
|
+
const update = useCallback(() => {
|
|
47
|
+
const anchor = anchorRef.current;
|
|
48
|
+
if (!anchor) return;
|
|
49
|
+
|
|
50
|
+
const rect = anchor.getBoundingClientRect();
|
|
51
|
+
const viewportHeight = window.innerHeight;
|
|
52
|
+
const viewportWidth = window.innerWidth;
|
|
53
|
+
const panelHeight = panelRef.current?.offsetHeight || estimatedHeight;
|
|
54
|
+
|
|
55
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
56
|
+
const spaceAbove = rect.top;
|
|
57
|
+
const flipAbove = spaceBelow < panelHeight + offset && spaceAbove > spaceBelow;
|
|
58
|
+
|
|
59
|
+
const width = matchWidth ? rect.width : undefined;
|
|
60
|
+
const panelWidth = panelRef.current?.offsetWidth ?? width ?? 0;
|
|
61
|
+
let left =
|
|
62
|
+
align === "end" && panelWidth
|
|
63
|
+
? rect.right - panelWidth
|
|
64
|
+
: rect.left;
|
|
65
|
+
if (panelWidth) {
|
|
66
|
+
left = Math.max(8, Math.min(left, viewportWidth - panelWidth - 8));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setStyle({
|
|
70
|
+
position: "fixed",
|
|
71
|
+
left,
|
|
72
|
+
width,
|
|
73
|
+
zIndex,
|
|
74
|
+
...(flipAbove
|
|
75
|
+
? { top: "auto", bottom: viewportHeight - rect.top + offset }
|
|
76
|
+
: { top: rect.bottom + offset, bottom: "auto" }),
|
|
77
|
+
});
|
|
78
|
+
}, [anchorRef, estimatedHeight, offset, matchWidth, align, zIndex]);
|
|
79
|
+
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
if (!isOpen) return;
|
|
82
|
+
|
|
83
|
+
update();
|
|
84
|
+
window.addEventListener("scroll", update, true);
|
|
85
|
+
window.addEventListener("resize", update);
|
|
86
|
+
|
|
87
|
+
const observer = new ResizeObserver(update);
|
|
88
|
+
if (panelRef.current) observer.observe(panelRef.current);
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("scroll", update, true);
|
|
92
|
+
window.removeEventListener("resize", update);
|
|
93
|
+
observer.disconnect();
|
|
94
|
+
};
|
|
95
|
+
}, [isOpen, update]);
|
|
96
|
+
|
|
97
|
+
return { panelRef, style };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default useFloatingPanel;
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,11 @@ import "./index.css";
|
|
|
4
4
|
import useClickOutside from "./hooks/useClickOutside";
|
|
5
5
|
import { useDebouncedSearch } from "./hooks/useDebouncedSearch";
|
|
6
6
|
import { useEditableRow } from "./hooks/useEditableRow";
|
|
7
|
+
import { useFloatingPanel } from "./hooks/useFloatingPanel";
|
|
7
8
|
import { useTableState, UseTableStateOptions, UseTableStateResult } from "./hooks/useTableState";
|
|
8
9
|
|
|
9
10
|
// COMPONENTES
|
|
11
|
+
import ITAccordion from "./components/molecules/accordion/accordion";
|
|
10
12
|
import ITAlert from "./components/molecules/alert/alert";
|
|
11
13
|
import ITAvatar from "./components/atoms/avatar/avatar";
|
|
12
14
|
import ITBadget from "./components/atoms/badget/badget";
|
|
@@ -15,6 +17,11 @@ import ITButton from "./components/atoms/button/button";
|
|
|
15
17
|
import ITCalendar from "./components/molecules/calendar/calendar";
|
|
16
18
|
import ITCard from "./components/molecules/card/card";
|
|
17
19
|
import ITCheckbox from "./components/atoms/checkbox/checkbox";
|
|
20
|
+
import ITChip from "./components/atoms/chip/chip";
|
|
21
|
+
import ITChipInput from "./components/molecules/chip-input/chip-input";
|
|
22
|
+
import ITDropdownMenu from "./components/molecules/dropdown-menu/dropdown-menu";
|
|
23
|
+
import ITField from "./components/molecules/field/field";
|
|
24
|
+
import ITMultiSelect from "./components/molecules/multi-select/multi-select";
|
|
18
25
|
import ITConfirmDialog from "./components/organisms/confirm-dialog/confirm-dialog";
|
|
19
26
|
import ITDataTable from "./components/organisms/data-table/dataTable";
|
|
20
27
|
import ITDatePicker from "./components/molecules/date-picker/datePicker";
|
|
@@ -51,6 +58,7 @@ import ITTextarea from "./components/atoms/textarea/textarea";
|
|
|
51
58
|
import ITTabs from "./components/molecules/tabs/tabs";
|
|
52
59
|
import ITTripleFilter from "./components/organisms/triple-filter/tripleFilter";
|
|
53
60
|
import ITToast from "./components/organisms/toast/toast";
|
|
61
|
+
import ITWysiwyg from "./components/molecules/wysiwyg/wysiwyg";
|
|
54
62
|
|
|
55
63
|
// PROPS
|
|
56
64
|
import type { ITAlertProps } from "./components/molecules/alert/alert.props";
|
|
@@ -61,7 +69,13 @@ import type { ITButtonProps } from "./components/atoms/button/button.props";
|
|
|
61
69
|
import type { ITCalendarProps } from "./components/molecules/calendar/calendar.props";
|
|
62
70
|
import type { ITCardProps } from "./components/molecules/card/card.props";
|
|
63
71
|
import type { ITCheckboxProps } from "./components/atoms/checkbox/checkbox.props";
|
|
72
|
+
import type { ITChipProps } from "./components/atoms/chip/chip.props";
|
|
73
|
+
import type { ITChipInputProps } from "./components/molecules/chip-input/chip-input.props";
|
|
64
74
|
import type { ITConfirmDialogProps } from "./components/organisms/confirm-dialog/confirm-dialog.props";
|
|
75
|
+
import type { ITAccordionProps, ITAccordionItem } from "./components/molecules/accordion/accordion.props";
|
|
76
|
+
import type { ITDropdownMenuProps, ITDropdownMenuItem } from "./components/molecules/dropdown-menu/dropdown-menu.props";
|
|
77
|
+
import type { ITFieldProps } from "./components/molecules/field/field.props";
|
|
78
|
+
import type { ITMultiSelectProps, ITMultiSelectOption } from "./components/molecules/multi-select/multi-select.props";
|
|
65
79
|
import type { ITDataTableFetchParams, ITDataTableProps, ITDataTableResponse } from "./components/organisms/data-table/dataTable.props";
|
|
66
80
|
import type { ITDatePickerProps } from "./components/molecules/date-picker/date-picker.props";
|
|
67
81
|
import type { ITDialogProps } from "./components/organisms/dialog/dialog.props";
|
|
@@ -97,6 +111,7 @@ import type { ITTextareaProps } from "./components/atoms/textarea/textarea.props
|
|
|
97
111
|
import type { ITTabsProps, ITTabItem } from "./components/molecules/tabs/tabs.props";
|
|
98
112
|
import type { ITTripleFilterProps, ITTripleFilterOption } from "./components/organisms/triple-filter/tripleFilter.props";
|
|
99
113
|
import type { ITToastProps } from "./components/organisms/toast/toast.props";
|
|
114
|
+
import type { ITWysiwygProps } from "./components/molecules/wysiwyg/wysiwyg.props";
|
|
100
115
|
import type { FieldConfig, FieldConfigV2 } from "./types/field.types";
|
|
101
116
|
|
|
102
117
|
// UTILS
|
|
@@ -118,12 +133,14 @@ import { getContrastTextColor, isLightColor, resolveCssColor } from "./utils/col
|
|
|
118
133
|
|
|
119
134
|
export {
|
|
120
135
|
// Hooks
|
|
121
|
-
useClickOutside, useDebouncedSearch, useEditableRow, useTableState,
|
|
136
|
+
useClickOutside, useDebouncedSearch, useEditableRow, useFloatingPanel, useTableState,
|
|
122
137
|
createValidationSchema,
|
|
123
138
|
// Components
|
|
139
|
+
ITAccordion,
|
|
124
140
|
ITAlert, ITAvatar, ITBadget, ITBreadcrumbs, ITButton,
|
|
125
141
|
ITCalendar,
|
|
126
|
-
ITCard, ITCheckbox, ITConfirmDialog, ITDataTable, ITDatePicker,
|
|
142
|
+
ITCard, ITCheckbox, ITChip, ITChipInput, ITConfirmDialog, ITDataTable, ITDatePicker,
|
|
143
|
+
ITDropdownMenu, ITField, ITMultiSelect,
|
|
127
144
|
ITDialog, ITDivider, ITDrawer,
|
|
128
145
|
ITDropfile, ITEmptyState, ITFlex, ITFormBuilder, ITFormHeader, ITGrid, ITImage, ITInput, ITLayout,
|
|
129
146
|
ITLoader, ITNavbar, ITPage, ITPageHeader, ITPagination, ITPopover, ITProgress,
|
|
@@ -131,7 +148,7 @@ export {
|
|
|
131
148
|
ITSearchTable,
|
|
132
149
|
ITSegmentedControl, ITSidebar, ITSkeleton, ITSlideToggle, ITSlider, ITStack, ITStatCard, ITStepper,
|
|
133
150
|
ITTable, ITTabs, ITText, ITTextarea, ITTripleFilter, ITThemeProvider,
|
|
134
|
-
useITTheme, useITThemeSafe, ITTimePicker, ITToast,
|
|
151
|
+
useITTheme, useITThemeSafe, ITTimePicker, ITToast, ITWysiwyg,
|
|
135
152
|
UploadStatus, FileTypeEnum,
|
|
136
153
|
getContrastTextColor, isLightColor, resolveCssColor
|
|
137
154
|
};
|
|
@@ -139,10 +156,12 @@ export {
|
|
|
139
156
|
export type {
|
|
140
157
|
Column, FieldConfig,
|
|
141
158
|
FieldConfigV2,
|
|
159
|
+
ITAccordionItem, ITAccordionProps,
|
|
142
160
|
ITAlertProps, ITAvatarProps, ITBadgetProps,
|
|
143
161
|
ITBreadcrumbItem, ITBreadcrumbsProps, ITButtonProps,
|
|
144
162
|
ITCalendarProps,
|
|
145
|
-
ITCardProps, ITCheckboxProps, ITConfirmDialogProps,
|
|
163
|
+
ITCardProps, ITCheckboxProps, ITChipProps, ITChipInputProps, ITConfirmDialogProps,
|
|
164
|
+
ITDropdownMenuItem, ITDropdownMenuProps, ITFieldProps, ITMultiSelectOption, ITMultiSelectProps,
|
|
146
165
|
ITDataTableFetchParams, ITDataTableProps, ITDataTableResponse, ITDatePickerProps,
|
|
147
166
|
ITDialogProps, ITDividerProps, ITDrawerProps,
|
|
148
167
|
ITEmptyStateProps, ITFlexProps,
|
|
@@ -159,6 +178,7 @@ export {
|
|
|
159
178
|
ITTableProps, ITTabsProps, ITTabItem, ITTextProps, ITTextareaProps,
|
|
160
179
|
ITTripleFilterProps, ITTripleFilterOption, ITThemeConfig, ITThemeProviderProps, ITThemePalette,
|
|
161
180
|
ITTimePickerProps, ITToastProps,
|
|
181
|
+
ITWysiwygProps,
|
|
162
182
|
UseTableStateOptions, UseTableStateResult
|
|
163
183
|
};
|
|
164
184
|
|