@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
@@ -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
- * Dynamic form generator from field definitions with validation support.
24
- * Renders a responsive grid of inputs, selects, and date pickers based on
25
- * the provided field configuration. Supports both legacy (`fields`) and
26
- * V2 (`config`) field definition formats.
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
- * @example
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: 6 },
32
- * { name: "role", label: "Role", type: "select", options: [{ id: 1, label: "Admin" }], column: 6 },
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={formValues}
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={v2FieldConfigs}
45
- * values={formValues}
46
- * handleChange={handleChange}
47
- * handleBlur={handleBlur}
48
- * touched={touched}
49
- * errors={errors}
50
- * isSubmitting={loading}
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 [, setMounted] = useState(false);
68
-
69
- useEffect(() => {
70
- setMounted(true);
71
- }, []);
72
-
73
- const providerValue = useMemo(() => ({
74
- config: config || [],
75
- values: values || {},
76
- errors: errors || {},
77
- touched: touched || {},
78
- handleChange,
79
- handleBlur,
80
- setFieldValue,
81
- setFieldTouched,
82
- setFieldError,
83
- initialValues: {},
84
- isSubmitting,
85
- isValidating: false,
86
- submitCount: 0,
87
- }), [config, values, errors, touched, handleChange, handleBlur, setFieldValue, setFieldTouched, setFieldError, isSubmitting]);
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 && setFieldValue) {
144
- onChangeAction(e.target.value, setFieldValue);
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 && setFieldValue) {
175
- onChangeAction(e.target.value, setFieldValue);
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 && setFieldValue) {
198
- onChangeAction(e.target.value, setFieldValue);
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 requiredFields = Object.keys(context.getFieldConfig).filter((name) => {
11
- const config = context.getFieldConfig(name);
12
- if (!config) return false;
13
- if (typeof config.required === 'function') {
14
- return config.required(values);
15
- }
16
- return config.required;
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.forEach((fieldName) => {
23
- const val = values[fieldName];
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[fieldName];
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, context]);
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: Object.keys(touched).length > 0,
64
+ isDirty,
36
65
  };
37
66
  };
38
67
 
39
68
  /**
40
- * Hook to evaluate dynamic rules using only the dependent values passed as props.
41
- * This avoids subscribing to the entire `values` context, enabling true React.memo isolation.
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 a filtered object containing only the fields this
44
- * field depends on (derived from `dependsOn` in FieldConfigV2).
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 || false;
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 || false;
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