@rachelallyson/hero-hook-form 2.6.0 → 2.7.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/CHANGELOG.md +35 -0
- package/dist/index.d.ts +374 -25
- package/dist/index.js +313 -67
- package/dist/react/index.d.ts +374 -25
- package/dist/react/index.js +313 -67
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -76,7 +76,7 @@ function useFormHelper({
|
|
|
76
76
|
|
|
77
77
|
// src/components/FormField.tsx
|
|
78
78
|
import React17 from "react";
|
|
79
|
-
import { useWatch as useWatch3 } from "react-hook-form";
|
|
79
|
+
import { get, useWatch as useWatch3 } from "react-hook-form";
|
|
80
80
|
|
|
81
81
|
// src/fields/AutocompleteField.tsx
|
|
82
82
|
import React from "react";
|
|
@@ -438,18 +438,23 @@ function FieldArrayField({
|
|
|
438
438
|
}) {
|
|
439
439
|
const {
|
|
440
440
|
addButtonText = "Add Item",
|
|
441
|
+
defaultItem,
|
|
442
|
+
enableReordering = false,
|
|
441
443
|
fields: fieldConfigs,
|
|
442
444
|
max = 10,
|
|
443
445
|
min = 0,
|
|
444
446
|
name,
|
|
445
|
-
removeButtonText = "Remove"
|
|
447
|
+
removeButtonText = "Remove",
|
|
448
|
+
renderAddButton,
|
|
449
|
+
renderItem,
|
|
450
|
+
reorderButtonText = { down: "\u2193", up: "\u2191" }
|
|
446
451
|
} = config;
|
|
447
452
|
const form = useFormContext3();
|
|
448
453
|
if (!form || !form.control) {
|
|
449
454
|
return null;
|
|
450
455
|
}
|
|
451
456
|
const { control } = form;
|
|
452
|
-
const { append, fields, remove } = useFieldArray({
|
|
457
|
+
const { append, fields, move, remove } = useFieldArray({
|
|
453
458
|
control,
|
|
454
459
|
name
|
|
455
460
|
// FieldArray name
|
|
@@ -458,18 +463,22 @@ function FieldArrayField({
|
|
|
458
463
|
const canRemove = fields.length > min;
|
|
459
464
|
const handleAdd = () => {
|
|
460
465
|
if (canAdd) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
466
|
+
if (defaultItem) {
|
|
467
|
+
append(defaultItem());
|
|
468
|
+
} else {
|
|
469
|
+
const defaultValues = fieldConfigs.reduce((acc, fieldConfig) => {
|
|
470
|
+
const fieldName = fieldConfig.name;
|
|
471
|
+
if (fieldConfig.type === "checkbox" || fieldConfig.type === "switch") {
|
|
472
|
+
acc[fieldName] = false;
|
|
473
|
+
} else if (fieldConfig.type === "slider") {
|
|
474
|
+
acc[fieldName] = 0;
|
|
475
|
+
} else {
|
|
476
|
+
acc[fieldName] = "";
|
|
477
|
+
}
|
|
478
|
+
return acc;
|
|
479
|
+
}, {});
|
|
480
|
+
append(defaultValues);
|
|
481
|
+
}
|
|
473
482
|
}
|
|
474
483
|
};
|
|
475
484
|
const handleRemove = (index) => {
|
|
@@ -477,60 +486,131 @@ function FieldArrayField({
|
|
|
477
486
|
remove(index);
|
|
478
487
|
}
|
|
479
488
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
489
|
+
const handleMoveUp = (index) => {
|
|
490
|
+
if (index > 0) {
|
|
491
|
+
move(index, index - 1);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const handleMoveDown = (index) => {
|
|
495
|
+
if (index < fields.length - 1) {
|
|
496
|
+
move(index, index + 1);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const renderFieldArrayItems = () => {
|
|
500
|
+
return fields.map((field2, index) => {
|
|
501
|
+
const canMoveUp = enableReordering && index > 0;
|
|
502
|
+
const canMoveDown = enableReordering && index < fields.length - 1;
|
|
503
|
+
const itemCanRemove = canRemove;
|
|
504
|
+
const fieldElements = fieldConfigs.map((fieldConfig) => {
|
|
505
|
+
const fieldName = fieldConfig.name;
|
|
506
|
+
const fullPath = `${name}.${index}.${fieldName}`;
|
|
507
|
+
let processedConfig = { ...fieldConfig, name: fullPath };
|
|
508
|
+
if ("dependsOn" in fieldConfig && fieldConfig.dependsOn && typeof fieldConfig.dependsOn === "string") {
|
|
509
|
+
const dependsOnPath = fieldConfig.dependsOn;
|
|
510
|
+
if (!dependsOnPath.startsWith(`${name}.`)) {
|
|
511
|
+
processedConfig = {
|
|
512
|
+
...processedConfig,
|
|
513
|
+
dependsOn: `${name}.${index}.${dependsOnPath}`,
|
|
514
|
+
// Preserve dependsOnValue if it exists
|
|
515
|
+
..."dependsOnValue" in fieldConfig && {
|
|
516
|
+
dependsOnValue: fieldConfig.dependsOnValue
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return /* @__PURE__ */ React8.createElement(
|
|
522
|
+
FormField,
|
|
523
|
+
{
|
|
524
|
+
key: `${fieldConfig.name}-${index}`,
|
|
525
|
+
config: processedConfig,
|
|
526
|
+
form,
|
|
527
|
+
submissionState: {
|
|
528
|
+
error: void 0,
|
|
529
|
+
isSubmitted: false,
|
|
530
|
+
isSubmitting: false,
|
|
531
|
+
isSuccess: false
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
if (renderItem) {
|
|
537
|
+
return /* @__PURE__ */ React8.createElement(React8.Fragment, { key: field2.id }, renderItem({
|
|
538
|
+
canMoveDown,
|
|
539
|
+
canMoveUp,
|
|
540
|
+
canRemove: itemCanRemove,
|
|
541
|
+
children: /* @__PURE__ */ React8.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4" }, fieldElements),
|
|
542
|
+
field: field2,
|
|
543
|
+
fields,
|
|
544
|
+
index,
|
|
545
|
+
onMoveDown: () => handleMoveDown(index),
|
|
546
|
+
onMoveUp: () => handleMoveUp(index),
|
|
547
|
+
onRemove: () => handleRemove(index)
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
return /* @__PURE__ */ React8.createElement(
|
|
551
|
+
"div",
|
|
552
|
+
{
|
|
553
|
+
key: field2.id,
|
|
554
|
+
className: "border border-gray-200 rounded-lg p-4 space-y-4"
|
|
555
|
+
},
|
|
556
|
+
/* @__PURE__ */ React8.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React8.createElement("h4", { className: "text-sm font-medium text-gray-700" }, config.label, " #", index + 1), /* @__PURE__ */ React8.createElement("div", { className: "flex gap-2" }, enableReordering && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(
|
|
557
|
+
Button2,
|
|
558
|
+
{
|
|
559
|
+
size: "sm",
|
|
560
|
+
variant: "light",
|
|
561
|
+
isDisabled: !canMoveUp,
|
|
562
|
+
onPress: () => handleMoveUp(index),
|
|
563
|
+
"aria-label": `Move ${config.label} ${index + 1} up`
|
|
564
|
+
},
|
|
565
|
+
reorderButtonText.up
|
|
566
|
+
), /* @__PURE__ */ React8.createElement(
|
|
567
|
+
Button2,
|
|
568
|
+
{
|
|
569
|
+
size: "sm",
|
|
570
|
+
variant: "light",
|
|
571
|
+
isDisabled: !canMoveDown,
|
|
572
|
+
onPress: () => handleMoveDown(index),
|
|
573
|
+
"aria-label": `Move ${config.label} ${index + 1} down`
|
|
574
|
+
},
|
|
575
|
+
reorderButtonText.down
|
|
576
|
+
)), itemCanRemove && /* @__PURE__ */ React8.createElement(
|
|
577
|
+
Button2,
|
|
578
|
+
{
|
|
579
|
+
size: "sm",
|
|
580
|
+
variant: "light",
|
|
581
|
+
color: "danger",
|
|
582
|
+
startContent: "\u{1F5D1}\uFE0F",
|
|
583
|
+
onPress: () => handleRemove(index),
|
|
584
|
+
"aria-label": `${removeButtonText} ${config.label} ${index + 1}`
|
|
585
|
+
},
|
|
586
|
+
removeButtonText
|
|
587
|
+
))),
|
|
588
|
+
/* @__PURE__ */ React8.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4" }, fieldElements)
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
const renderAddButtonElement = () => {
|
|
593
|
+
if (renderAddButton) {
|
|
594
|
+
return renderAddButton({
|
|
595
|
+
canAdd,
|
|
596
|
+
onAdd: handleAdd
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (!canAdd) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return /* @__PURE__ */ React8.createElement(
|
|
487
603
|
Button2,
|
|
488
604
|
{
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
onPress: () => handleRemove(index),
|
|
494
|
-
"aria-label": `${removeButtonText} ${config.label} ${index + 1}`
|
|
605
|
+
variant: "bordered",
|
|
606
|
+
startContent: "\u2795",
|
|
607
|
+
onPress: handleAdd,
|
|
608
|
+
className: "w-full"
|
|
495
609
|
},
|
|
496
|
-
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
{
|
|
501
|
-
key: `${fieldConfig.name}-${index}`,
|
|
502
|
-
config: {
|
|
503
|
-
...fieldConfig,
|
|
504
|
-
name: `${name}.${index}.${fieldConfig.name}`
|
|
505
|
-
},
|
|
506
|
-
form,
|
|
507
|
-
submissionState: {
|
|
508
|
-
error: void 0,
|
|
509
|
-
isSubmitted: false,
|
|
510
|
-
isSubmitting: false,
|
|
511
|
-
isSuccess: false
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
)))
|
|
515
|
-
)), canAdd && /* @__PURE__ */ React8.createElement(
|
|
516
|
-
Button2,
|
|
517
|
-
{
|
|
518
|
-
variant: "bordered",
|
|
519
|
-
startContent: "\u2795",
|
|
520
|
-
onPress: handleAdd,
|
|
521
|
-
className: "w-full"
|
|
522
|
-
},
|
|
523
|
-
addButtonText
|
|
524
|
-
), fields.length === 0 && /* @__PURE__ */ React8.createElement("div", { className: "text-center py-8 text-gray-500" }, /* @__PURE__ */ React8.createElement("p", null, "No ", config.label?.toLowerCase(), " added yet."), /* @__PURE__ */ React8.createElement(
|
|
525
|
-
Button2,
|
|
526
|
-
{
|
|
527
|
-
variant: "bordered",
|
|
528
|
-
startContent: "\u2795",
|
|
529
|
-
onPress: handleAdd,
|
|
530
|
-
className: "mt-2"
|
|
531
|
-
},
|
|
532
|
-
addButtonText
|
|
533
|
-
))));
|
|
610
|
+
addButtonText
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
return /* @__PURE__ */ React8.createElement("div", { className }, /* @__PURE__ */ React8.createElement("div", { className: "space-y-4" }, fields.length > 0 ? renderFieldArrayItems() : /* @__PURE__ */ React8.createElement("div", { className: "text-center py-8 text-gray-500" }, /* @__PURE__ */ React8.createElement("p", null, "No ", config.label?.toLowerCase(), " added yet."), renderAddButtonElement()), fields.length > 0 && renderAddButtonElement()));
|
|
534
614
|
}
|
|
535
615
|
|
|
536
616
|
// src/fields/FileField.tsx
|
|
@@ -1056,7 +1136,7 @@ var FormField = React17.memo(
|
|
|
1056
1136
|
return null;
|
|
1057
1137
|
}
|
|
1058
1138
|
if (config.dependsOn) {
|
|
1059
|
-
const dependentValue = watchedValues
|
|
1139
|
+
const dependentValue = get(watchedValues, config.dependsOn);
|
|
1060
1140
|
if (config.dependsOnValue !== void 0 && dependentValue !== config.dependsOnValue) {
|
|
1061
1141
|
return null;
|
|
1062
1142
|
}
|
|
@@ -2340,6 +2420,44 @@ function ZodForm({
|
|
|
2340
2420
|
))));
|
|
2341
2421
|
}
|
|
2342
2422
|
|
|
2423
|
+
// src/components/SimpleForm.tsx
|
|
2424
|
+
import React24 from "react";
|
|
2425
|
+
function SimpleForm({
|
|
2426
|
+
className,
|
|
2427
|
+
defaultValues,
|
|
2428
|
+
field: field2,
|
|
2429
|
+
hideSubmitButton = false,
|
|
2430
|
+
onError,
|
|
2431
|
+
onSubmit,
|
|
2432
|
+
onSuccess,
|
|
2433
|
+
schema,
|
|
2434
|
+
submitButton,
|
|
2435
|
+
subtitle,
|
|
2436
|
+
title
|
|
2437
|
+
}) {
|
|
2438
|
+
return /* @__PURE__ */ React24.createElement(
|
|
2439
|
+
ZodForm,
|
|
2440
|
+
{
|
|
2441
|
+
className,
|
|
2442
|
+
config: {
|
|
2443
|
+
defaultValues,
|
|
2444
|
+
fields: [field2],
|
|
2445
|
+
schema
|
|
2446
|
+
},
|
|
2447
|
+
onError,
|
|
2448
|
+
onSubmit,
|
|
2449
|
+
onSuccess,
|
|
2450
|
+
showResetButton: false,
|
|
2451
|
+
submitButtonText: hideSubmitButton ? "" : "Submit",
|
|
2452
|
+
subtitle,
|
|
2453
|
+
title,
|
|
2454
|
+
submitButtonProps: hideSubmitButton && submitButton ? {
|
|
2455
|
+
style: { display: "none" }
|
|
2456
|
+
} : {}
|
|
2457
|
+
}
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2343
2461
|
// src/builders/BasicFormBuilder.ts
|
|
2344
2462
|
var BasicFormBuilder = class {
|
|
2345
2463
|
constructor() {
|
|
@@ -3748,6 +3866,131 @@ function useMemoizedFieldProps(props, deps) {
|
|
|
3748
3866
|
return useMemo2(() => props, deps);
|
|
3749
3867
|
}
|
|
3750
3868
|
|
|
3869
|
+
// src/utils/arraySync.ts
|
|
3870
|
+
function syncArrays(options) {
|
|
3871
|
+
const { current, existing, getId } = options;
|
|
3872
|
+
const existingMap = /* @__PURE__ */ new Map();
|
|
3873
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
3874
|
+
existing.forEach((item) => {
|
|
3875
|
+
const id = getId(item);
|
|
3876
|
+
if (id !== void 0) {
|
|
3877
|
+
existingMap.set(id, item);
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
current.forEach((item) => {
|
|
3881
|
+
const id = getId(item);
|
|
3882
|
+
if (id !== void 0) {
|
|
3883
|
+
currentMap.set(id, item);
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
const toDelete = [];
|
|
3887
|
+
existingMap.forEach((item, id) => {
|
|
3888
|
+
if (!currentMap.has(id)) {
|
|
3889
|
+
toDelete.push(item);
|
|
3890
|
+
}
|
|
3891
|
+
});
|
|
3892
|
+
const toUpdate = [];
|
|
3893
|
+
existingMap.forEach((existingItem, id) => {
|
|
3894
|
+
const currentItem = currentMap.get(id);
|
|
3895
|
+
if (currentItem) {
|
|
3896
|
+
toUpdate.push({ current: currentItem, existing: existingItem });
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
const toCreate = [];
|
|
3900
|
+
currentMap.forEach((item, id) => {
|
|
3901
|
+
if (!existingMap.has(id)) {
|
|
3902
|
+
toCreate.push(item);
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
3905
|
+
return {
|
|
3906
|
+
toCreate,
|
|
3907
|
+
toDelete,
|
|
3908
|
+
toUpdate
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// src/utils/createFieldArrayCustomConfig.tsx
|
|
3913
|
+
import React25 from "react";
|
|
3914
|
+
import { useFieldArray as useFieldArray2 } from "react-hook-form";
|
|
3915
|
+
import { Button as Button6 } from "@heroui/react";
|
|
3916
|
+
function createFieldArrayCustomConfig(options) {
|
|
3917
|
+
const {
|
|
3918
|
+
className,
|
|
3919
|
+
defaultItem,
|
|
3920
|
+
enableReordering = false,
|
|
3921
|
+
label,
|
|
3922
|
+
max = 10,
|
|
3923
|
+
min = 0,
|
|
3924
|
+
name,
|
|
3925
|
+
renderAddButton,
|
|
3926
|
+
renderItem
|
|
3927
|
+
} = options;
|
|
3928
|
+
return {
|
|
3929
|
+
className,
|
|
3930
|
+
label,
|
|
3931
|
+
name,
|
|
3932
|
+
// ArrayPath is compatible with Path for CustomFieldConfig
|
|
3933
|
+
render: ({ control, errors, form }) => {
|
|
3934
|
+
const { append, fields, move, remove } = useFieldArray2({
|
|
3935
|
+
control,
|
|
3936
|
+
name
|
|
3937
|
+
});
|
|
3938
|
+
const canAdd = fields.length < max;
|
|
3939
|
+
const canRemove = fields.length > min;
|
|
3940
|
+
const handleAdd = () => {
|
|
3941
|
+
if (canAdd) {
|
|
3942
|
+
if (defaultItem) {
|
|
3943
|
+
append(defaultItem());
|
|
3944
|
+
} else {
|
|
3945
|
+
append({});
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
};
|
|
3949
|
+
const handleRemove = (index) => {
|
|
3950
|
+
if (canRemove) {
|
|
3951
|
+
remove(index);
|
|
3952
|
+
}
|
|
3953
|
+
};
|
|
3954
|
+
const handleMoveUp = (index) => {
|
|
3955
|
+
if (enableReordering && index > 0) {
|
|
3956
|
+
move(index, index - 1);
|
|
3957
|
+
}
|
|
3958
|
+
};
|
|
3959
|
+
const handleMoveDown = (index) => {
|
|
3960
|
+
if (enableReordering && index < fields.length - 1) {
|
|
3961
|
+
move(index, index + 1);
|
|
3962
|
+
}
|
|
3963
|
+
};
|
|
3964
|
+
return /* @__PURE__ */ React25.createElement("div", { className }, /* @__PURE__ */ React25.createElement("div", { className: "space-y-4" }, fields.map((field2, index) => {
|
|
3965
|
+
const canMoveUp = enableReordering && index > 0;
|
|
3966
|
+
const canMoveDown = enableReordering && index < fields.length - 1;
|
|
3967
|
+
return /* @__PURE__ */ React25.createElement(React25.Fragment, { key: field2.id }, renderItem({
|
|
3968
|
+
canMoveDown,
|
|
3969
|
+
canMoveUp,
|
|
3970
|
+
control,
|
|
3971
|
+
errors,
|
|
3972
|
+
field: field2,
|
|
3973
|
+
fields,
|
|
3974
|
+
form,
|
|
3975
|
+
index,
|
|
3976
|
+
onMoveDown: () => handleMoveDown(index),
|
|
3977
|
+
onMoveUp: () => handleMoveUp(index),
|
|
3978
|
+
onRemove: () => handleRemove(index)
|
|
3979
|
+
}));
|
|
3980
|
+
}), fields.length === 0 && renderAddButton ? /* @__PURE__ */ React25.createElement("div", { className: "text-center py-8 text-gray-500" }, /* @__PURE__ */ React25.createElement("p", null, "No ", label?.toLowerCase() || "items", " added yet."), renderAddButton({ canAdd, onAdd: handleAdd })) : null, fields.length > 0 && renderAddButton ? renderAddButton({ canAdd, onAdd: handleAdd }) : canAdd && /* @__PURE__ */ React25.createElement(
|
|
3981
|
+
Button6,
|
|
3982
|
+
{
|
|
3983
|
+
variant: "bordered",
|
|
3984
|
+
onPress: handleAdd,
|
|
3985
|
+
className: "w-full"
|
|
3986
|
+
},
|
|
3987
|
+
"Add Item"
|
|
3988
|
+
)));
|
|
3989
|
+
},
|
|
3990
|
+
type: "custom"
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3751
3994
|
// src/builders/validation-helpers.ts
|
|
3752
3995
|
import { z as z4 } from "zod";
|
|
3753
3996
|
var validationPatterns = {
|
|
@@ -3921,6 +4164,7 @@ export {
|
|
|
3921
4164
|
RadioGroupField,
|
|
3922
4165
|
SelectField,
|
|
3923
4166
|
ServerActionForm,
|
|
4167
|
+
SimpleForm,
|
|
3924
4168
|
SliderField,
|
|
3925
4169
|
SubmitButton,
|
|
3926
4170
|
SwitchField,
|
|
@@ -3936,6 +4180,7 @@ export {
|
|
|
3936
4180
|
createEmailSchema,
|
|
3937
4181
|
createField,
|
|
3938
4182
|
createFieldArrayBuilder,
|
|
4183
|
+
createFieldArrayCustomConfig,
|
|
3939
4184
|
createFieldArrayItemBuilder,
|
|
3940
4185
|
createFileSchema,
|
|
3941
4186
|
createFormTestUtils,
|
|
@@ -3969,6 +4214,7 @@ export {
|
|
|
3969
4214
|
shallowEqual,
|
|
3970
4215
|
simulateFieldInput,
|
|
3971
4216
|
simulateFormSubmission,
|
|
4217
|
+
syncArrays,
|
|
3972
4218
|
throttle,
|
|
3973
4219
|
useDebouncedFieldValidation,
|
|
3974
4220
|
useDebouncedValidation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rachelallyson/hero-hook-form",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Typed form helpers that combine React Hook Form and HeroUI components.",
|
|
5
5
|
"author": "Rachel Higley",
|
|
6
6
|
"homepage": "https://rachelallyson.github.io/hero-hook-form/",
|