@lub-crm/forms 1.0.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 (52) hide show
  1. package/README.md +298 -0
  2. package/dist/lub-forms.css +1 -0
  3. package/dist/lub-forms.es.js +5848 -0
  4. package/dist/lub-forms.es.js.map +1 -0
  5. package/dist/lub-forms.standalone.js +10 -0
  6. package/dist/lub-forms.standalone.js.map +1 -0
  7. package/dist/lub-forms.umd.js +227 -0
  8. package/dist/lub-forms.umd.js.map +1 -0
  9. package/package.json +68 -0
  10. package/src/api/client.ts +115 -0
  11. package/src/api/index.ts +2 -0
  12. package/src/api/types.ts +202 -0
  13. package/src/core/FormProvider.tsx +228 -0
  14. package/src/core/FormRenderer.tsx +134 -0
  15. package/src/core/LubForm.tsx +476 -0
  16. package/src/core/StepManager.tsx +199 -0
  17. package/src/core/index.ts +4 -0
  18. package/src/embed.ts +188 -0
  19. package/src/fields/CheckboxField.tsx +62 -0
  20. package/src/fields/CheckboxGroupField.tsx +57 -0
  21. package/src/fields/CountryField.tsx +43 -0
  22. package/src/fields/DateField.tsx +33 -0
  23. package/src/fields/DateTimeField.tsx +33 -0
  24. package/src/fields/DividerField.tsx +16 -0
  25. package/src/fields/FieldWrapper.tsx +60 -0
  26. package/src/fields/FileField.tsx +45 -0
  27. package/src/fields/HiddenField.tsx +18 -0
  28. package/src/fields/HtmlField.tsx +17 -0
  29. package/src/fields/NumberField.tsx +39 -0
  30. package/src/fields/RadioField.tsx +57 -0
  31. package/src/fields/RecaptchaField.tsx +137 -0
  32. package/src/fields/SelectField.tsx +49 -0
  33. package/src/fields/StateField.tsx +84 -0
  34. package/src/fields/TextField.tsx +51 -0
  35. package/src/fields/TextareaField.tsx +37 -0
  36. package/src/fields/TimeField.tsx +33 -0
  37. package/src/fields/index.ts +84 -0
  38. package/src/hooks/index.ts +4 -0
  39. package/src/hooks/useConditionalLogic.ts +59 -0
  40. package/src/hooks/useFormApi.ts +118 -0
  41. package/src/hooks/useFormDesign.ts +48 -0
  42. package/src/hooks/useMultiStep.ts +98 -0
  43. package/src/index.ts +101 -0
  44. package/src/main.tsx +40 -0
  45. package/src/styles/index.css +707 -0
  46. package/src/utils/cn.ts +6 -0
  47. package/src/utils/countries.ts +163 -0
  48. package/src/utils/css-variables.ts +63 -0
  49. package/src/utils/index.ts +3 -0
  50. package/src/validation/conditional.ts +170 -0
  51. package/src/validation/index.ts +2 -0
  52. package/src/validation/schema-builder.ts +327 -0
@@ -0,0 +1,199 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "@/utils/cn";
3
+ import type { FormStep } from "@/api/types";
4
+
5
+ interface StepManagerProps {
6
+ steps: FormStep[];
7
+ currentStep: number;
8
+ onStepClick?: (stepIndex: number) => void;
9
+ allowStepNavigation?: boolean;
10
+ }
11
+
12
+ export function StepManager({
13
+ steps,
14
+ currentStep,
15
+ onStepClick,
16
+ allowStepNavigation = false,
17
+ }: StepManagerProps) {
18
+ if (steps.length <= 1) return null;
19
+
20
+ return (
21
+ <div className="lub-form__steps" role="navigation" aria-label="Form steps">
22
+ <ol className="lub-form__steps-list">
23
+ {steps.map((step, index) => {
24
+ const isActive = index === currentStep;
25
+ const isCompleted = index < currentStep;
26
+ const isClickable = allowStepNavigation && (isCompleted || isActive);
27
+
28
+ return (
29
+ <li
30
+ key={step.id}
31
+ className={cn(
32
+ "lub-form__step",
33
+ isActive && "lub-form__step--active",
34
+ isCompleted && "lub-form__step--completed",
35
+ )}
36
+ >
37
+ {isClickable ? (
38
+ <button
39
+ type="button"
40
+ className="lub-form__step-button"
41
+ onClick={() => onStepClick?.(index)}
42
+ aria-current={isActive ? "step" : undefined}
43
+ >
44
+ <span className="lub-form__step-number">{index + 1}</span>
45
+ <span className="lub-form__step-name">{step.name}</span>
46
+ </button>
47
+ ) : (
48
+ <span
49
+ className="lub-form__step-indicator"
50
+ aria-current={isActive ? "step" : undefined}
51
+ >
52
+ <span className="lub-form__step-number">
53
+ {isCompleted ? <CheckIcon /> : index + 1}
54
+ </span>
55
+ <span className="lub-form__step-name">{step.name}</span>
56
+ </span>
57
+ )}
58
+ {index < steps.length - 1 && (
59
+ <span
60
+ className={cn(
61
+ "lub-form__step-connector",
62
+ isCompleted && "lub-form__step-connector--completed",
63
+ )}
64
+ aria-hidden="true"
65
+ />
66
+ )}
67
+ </li>
68
+ );
69
+ })}
70
+ </ol>
71
+ {steps[currentStep]?.description && (
72
+ <p className="lub-form__step-description">
73
+ {steps[currentStep].description}
74
+ </p>
75
+ )}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function CheckIcon() {
81
+ return (
82
+ <svg
83
+ className="lub-form__check-icon"
84
+ viewBox="0 0 20 20"
85
+ fill="currentColor"
86
+ aria-hidden="true"
87
+ >
88
+ <path
89
+ fillRule="evenodd"
90
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
91
+ clipRule="evenodd"
92
+ />
93
+ </svg>
94
+ );
95
+ }
96
+
97
+ // Step navigation buttons
98
+ interface StepNavigationProps {
99
+ currentStep: number;
100
+ totalSteps: number;
101
+ onNext: () => void;
102
+ onPrev: () => void;
103
+ isNextDisabled?: boolean;
104
+ isPrevDisabled?: boolean;
105
+ isLastStep?: boolean;
106
+ isSubmitting?: boolean;
107
+ submitButtonText?: string;
108
+ nextButtonText?: string;
109
+ prevButtonText?: string;
110
+ children?: ReactNode;
111
+ }
112
+
113
+ export function StepNavigation({
114
+ currentStep,
115
+ totalSteps,
116
+ onNext,
117
+ onPrev,
118
+ isNextDisabled = false,
119
+ isPrevDisabled = false,
120
+ isLastStep = false,
121
+ isSubmitting = false,
122
+ submitButtonText = "Submit",
123
+ nextButtonText = "Next",
124
+ prevButtonText = "Back",
125
+ children,
126
+ }: StepNavigationProps) {
127
+ const showPrev = currentStep > 0;
128
+ const showNext = !isLastStep;
129
+
130
+ return (
131
+ <div className="lub-form__navigation">
132
+ {showPrev && (
133
+ <button
134
+ type="button"
135
+ className="lub-form__button lub-form__button--secondary"
136
+ onClick={onPrev}
137
+ disabled={isPrevDisabled || isSubmitting}
138
+ >
139
+ {prevButtonText}
140
+ </button>
141
+ )}
142
+ <div className="lub-form__navigation-spacer" />
143
+ {showNext ? (
144
+ <button
145
+ type="button"
146
+ className="lub-form__button lub-form__button--primary"
147
+ onClick={onNext}
148
+ disabled={isNextDisabled || isSubmitting}
149
+ >
150
+ {nextButtonText}
151
+ </button>
152
+ ) : (
153
+ (children ?? (
154
+ <button
155
+ type="submit"
156
+ className="lub-form__button lub-form__button--primary"
157
+ disabled={isSubmitting}
158
+ >
159
+ {isSubmitting ? (
160
+ <span className="lub-form__button-loading">
161
+ <LoadingSpinner />
162
+ Submitting...
163
+ </span>
164
+ ) : (
165
+ submitButtonText
166
+ )}
167
+ </button>
168
+ ))
169
+ )}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ function LoadingSpinner() {
175
+ return (
176
+ <svg
177
+ className="lub-form__spinner"
178
+ viewBox="0 0 24 24"
179
+ fill="none"
180
+ aria-hidden="true"
181
+ >
182
+ <circle
183
+ className="lub-form__spinner-track"
184
+ cx="12"
185
+ cy="12"
186
+ r="10"
187
+ stroke="currentColor"
188
+ strokeWidth="3"
189
+ />
190
+ <path
191
+ className="lub-form__spinner-head"
192
+ d="M12 2a10 10 0 0 1 10 10"
193
+ stroke="currentColor"
194
+ strokeWidth="3"
195
+ strokeLinecap="round"
196
+ />
197
+ </svg>
198
+ );
199
+ }
@@ -0,0 +1,4 @@
1
+ export { LubForm, type LubFormProps } from "./LubForm";
2
+ export { LubFormProvider, useLubFormContext } from "./FormProvider";
3
+ export { FormRenderer } from "./FormRenderer";
4
+ export { StepManager, StepNavigation } from "./StepManager";
package/src/embed.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * UMD/IIFE entry point for script tag embedding
3
+ *
4
+ * Usage:
5
+ *
6
+ * <!-- Auto-mount via data attributes -->
7
+ * <script src="https://forms.lub.com/v1/lub-forms.standalone.js"></script>
8
+ * <link rel="stylesheet" href="https://forms.lub.com/v1/lub-forms.css">
9
+ * <div data-lub-form-id="abc123" data-lub-base-url="https://api.lub.com"></div>
10
+ *
11
+ * <!-- Manual render -->
12
+ * <script>
13
+ * LubForms.render('abc123', 'container-id', {
14
+ * baseUrl: 'https://api.lub.com',
15
+ * onSuccess: (data) => console.log('Success!', data),
16
+ * onError: (err) => console.error('Error:', err),
17
+ * });
18
+ * </script>
19
+ */
20
+
21
+ import { createElement } from "react";
22
+ import { createRoot, type Root } from "react-dom/client";
23
+ import { LubForm, type LubFormProps } from "./core/LubForm";
24
+ import type { SubmitFormResponse, ApiError, FormDesign } from "./api/types";
25
+
26
+ // Import styles for standalone bundle
27
+ import "./styles/index.css";
28
+
29
+ // Store roots for cleanup
30
+ const roots = new Map<string, Root>();
31
+
32
+ // Render options type
33
+ interface RenderOptions {
34
+ baseUrl?: string;
35
+ onSuccess?: (data: SubmitFormResponse) => void;
36
+ onError?: (error: ApiError) => void;
37
+ onValidationError?: (errors: Record<string, string>) => void;
38
+ onStepChange?: (step: number, total: number) => void;
39
+ className?: string;
40
+ designOverrides?: Partial<FormDesign>;
41
+ }
42
+
43
+ /**
44
+ * Render a form into a container element
45
+ */
46
+ function render(
47
+ formId: string,
48
+ containerOrId: string | HTMLElement,
49
+ options: RenderOptions = {},
50
+ ): { unmount: () => void } {
51
+ const container =
52
+ typeof containerOrId === "string"
53
+ ? document.getElementById(containerOrId)
54
+ : containerOrId;
55
+
56
+ if (!container) {
57
+ console.error(`[LubForms] Container not found: ${containerOrId}`);
58
+ return { unmount: () => {} };
59
+ }
60
+
61
+ // Unmount existing root if present
62
+ const existingRoot = roots.get(container.id || formId);
63
+ if (existingRoot) {
64
+ existingRoot.unmount();
65
+ roots.delete(container.id || formId);
66
+ }
67
+
68
+ // Create new root and render
69
+ const root = createRoot(container);
70
+ roots.set(container.id || formId, root);
71
+
72
+ const props: LubFormProps = {
73
+ formId,
74
+ baseUrl: options.baseUrl || "",
75
+ onSuccess: options.onSuccess,
76
+ onError: options.onError,
77
+ onValidationError: options.onValidationError,
78
+ onStepChange: options.onStepChange,
79
+ className: options.className,
80
+ designOverrides: options.designOverrides,
81
+ };
82
+
83
+ root.render(createElement(LubForm, props));
84
+
85
+ return {
86
+ unmount: () => {
87
+ root.unmount();
88
+ roots.delete(container.id || formId);
89
+ },
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Auto-mount forms from data attributes
95
+ */
96
+ function autoMount(): void {
97
+ const containers =
98
+ document.querySelectorAll<HTMLElement>("[data-lub-form-id]");
99
+
100
+ containers.forEach((container) => {
101
+ const formId = container.dataset.lubFormId;
102
+ if (!formId) return;
103
+
104
+ // Skip if already mounted
105
+ if (container.dataset.lubMounted === "true") return;
106
+
107
+ const options: RenderOptions = {
108
+ baseUrl: container.dataset.lubBaseUrl || "",
109
+ className: container.dataset.lubClass,
110
+ };
111
+
112
+ // Handle callbacks via global functions
113
+ const onSuccessName = container.dataset.lubOnSuccess;
114
+ if (
115
+ onSuccessName &&
116
+ typeof window[onSuccessName as keyof Window] === "function"
117
+ ) {
118
+ options.onSuccess = window[onSuccessName as keyof Window] as (
119
+ data: SubmitFormResponse,
120
+ ) => void;
121
+ }
122
+
123
+ const onErrorName = container.dataset.lubOnError;
124
+ if (
125
+ onErrorName &&
126
+ typeof window[onErrorName as keyof Window] === "function"
127
+ ) {
128
+ options.onError = window[onErrorName as keyof Window] as (
129
+ error: ApiError,
130
+ ) => void;
131
+ }
132
+
133
+ render(formId, container, options);
134
+ container.dataset.lubMounted = "true";
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Unmount a form by container ID
140
+ */
141
+ function unmount(containerId: string): void {
142
+ const root = roots.get(containerId);
143
+ if (root) {
144
+ root.unmount();
145
+ roots.delete(containerId);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Unmount all forms
151
+ */
152
+ function unmountAll(): void {
153
+ roots.forEach((root) => root.unmount());
154
+ roots.clear();
155
+ }
156
+
157
+ /**
158
+ * Initialize LubForms (call autoMount on DOM ready)
159
+ */
160
+ function init(): void {
161
+ if (document.readyState === "loading") {
162
+ document.addEventListener("DOMContentLoaded", autoMount);
163
+ } else {
164
+ autoMount();
165
+ }
166
+ }
167
+
168
+ // Export for UMD/global
169
+ const LubForms = {
170
+ render,
171
+ autoMount,
172
+ unmount,
173
+ unmountAll,
174
+ init,
175
+ // Re-export main component for advanced usage
176
+ LubForm,
177
+ };
178
+
179
+ // Auto-initialize when script loads
180
+ init();
181
+
182
+ // Expose on window for UMD
183
+ if (typeof window !== "undefined") {
184
+ (window as unknown as { LubForms: typeof LubForms }).LubForms = LubForms;
185
+ }
186
+
187
+ export default LubForms;
188
+ export { render, autoMount, unmount, unmountAll, init, LubForm };
@@ -0,0 +1,62 @@
1
+ import { useFormContext } from "react-hook-form";
2
+ import type { FormField } from "@/api/types";
3
+ import { cn } from "@/utils/cn";
4
+
5
+ interface CheckboxFieldProps {
6
+ field: FormField;
7
+ }
8
+
9
+ export function CheckboxField({ field }: CheckboxFieldProps) {
10
+ const { register, formState } = useFormContext();
11
+ const error = formState.errors[field.name];
12
+ const hasError = !!error;
13
+
14
+ return (
15
+ <div
16
+ className={cn(
17
+ "lub-form__field-wrapper",
18
+ hasError && "lub-form__field-wrapper--error",
19
+ )}
20
+ >
21
+ <label className="lub-form__checkbox-label">
22
+ <input
23
+ id={field.name}
24
+ type="checkbox"
25
+ className={cn(
26
+ "lub-form__checkbox",
27
+ hasError && "lub-form__checkbox--error",
28
+ )}
29
+ aria-invalid={hasError}
30
+ aria-describedby={
31
+ hasError
32
+ ? `${field.name}-error`
33
+ : field.help_text
34
+ ? `${field.name}-help`
35
+ : undefined
36
+ }
37
+ {...register(field.name)}
38
+ />
39
+ <span className="lub-form__checkbox-text">
40
+ {field.label}
41
+ {field.required && (
42
+ <span className="lub-form__required-indicator" aria-hidden="true">
43
+ *
44
+ </span>
45
+ )}
46
+ </span>
47
+ </label>
48
+
49
+ {field.help_text && !hasError && (
50
+ <p className="lub-form__help-text" id={`${field.name}-help`}>
51
+ {field.help_text}
52
+ </p>
53
+ )}
54
+
55
+ {hasError && (
56
+ <p className="lub-form__error" id={`${field.name}-error`} role="alert">
57
+ {error.message as string}
58
+ </p>
59
+ )}
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,57 @@
1
+ import { useFormContext } from "react-hook-form";
2
+ import type { FormField } from "@/api/types";
3
+ import { FieldWrapper } from "./FieldWrapper";
4
+ import { cn } from "@/utils/cn";
5
+
6
+ interface CheckboxGroupFieldProps {
7
+ field: FormField;
8
+ }
9
+
10
+ export function CheckboxGroupField({ field }: CheckboxGroupFieldProps) {
11
+ const { register, formState } = useFormContext();
12
+ const error = formState.errors[field.name];
13
+ const hasError = !!error;
14
+
15
+ const options = field.options?.options ?? [];
16
+
17
+ return (
18
+ <FieldWrapper field={field}>
19
+ <div
20
+ className="lub-form__checkbox-group"
21
+ role="group"
22
+ aria-labelledby={`${field.name}-label`}
23
+ >
24
+ {options.map((option) => (
25
+ <label key={option.value} className="lub-form__checkbox-label">
26
+ <input
27
+ type="checkbox"
28
+ className={cn(
29
+ "lub-form__checkbox",
30
+ hasError && "lub-form__checkbox--error",
31
+ )}
32
+ value={option.value}
33
+ {...register(field.name)}
34
+ />
35
+ <span className="lub-form__checkbox-text">{option.label}</span>
36
+ </label>
37
+ ))}
38
+ {field.options?.allow_other && (
39
+ <label className="lub-form__checkbox-label">
40
+ <input
41
+ type="checkbox"
42
+ className={cn(
43
+ "lub-form__checkbox",
44
+ hasError && "lub-form__checkbox--error",
45
+ )}
46
+ value="__other__"
47
+ {...register(field.name)}
48
+ />
49
+ <span className="lub-form__checkbox-text">
50
+ {field.options.other_label || "Other"}
51
+ </span>
52
+ </label>
53
+ )}
54
+ </div>
55
+ </FieldWrapper>
56
+ );
57
+ }
@@ -0,0 +1,43 @@
1
+ import { useFormContext } from "react-hook-form";
2
+ import type { FormField } from "@/api/types";
3
+ import { FieldWrapper } from "./FieldWrapper";
4
+ import { cn } from "@/utils/cn";
5
+ import { countries } from "@/utils/countries";
6
+
7
+ interface CountryFieldProps {
8
+ field: FormField;
9
+ }
10
+
11
+ export function CountryField({ field }: CountryFieldProps) {
12
+ const { register, formState } = useFormContext();
13
+ const error = formState.errors[field.name];
14
+ const hasError = !!error;
15
+
16
+ return (
17
+ <FieldWrapper field={field}>
18
+ <select
19
+ id={field.name}
20
+ className={cn(
21
+ "lub-form__select",
22
+ hasError && "lub-form__select--error",
23
+ )}
24
+ aria-invalid={hasError}
25
+ aria-describedby={
26
+ hasError
27
+ ? `${field.name}-error`
28
+ : field.help_text
29
+ ? `${field.name}-help`
30
+ : undefined
31
+ }
32
+ {...register(field.name)}
33
+ >
34
+ <option value="">{field.placeholder || "Select a country..."}</option>
35
+ {countries.map((country) => (
36
+ <option key={country.code} value={country.code}>
37
+ {country.name}
38
+ </option>
39
+ ))}
40
+ </select>
41
+ </FieldWrapper>
42
+ );
43
+ }
@@ -0,0 +1,33 @@
1
+ import { useFormContext } from "react-hook-form";
2
+ import type { FormField } from "@/api/types";
3
+ import { FieldWrapper } from "./FieldWrapper";
4
+ import { cn } from "@/utils/cn";
5
+
6
+ interface DateFieldProps {
7
+ field: FormField;
8
+ }
9
+
10
+ export function DateField({ field }: DateFieldProps) {
11
+ const { register, formState } = useFormContext();
12
+ const error = formState.errors[field.name];
13
+ const hasError = !!error;
14
+
15
+ return (
16
+ <FieldWrapper field={field}>
17
+ <input
18
+ id={field.name}
19
+ type="date"
20
+ className={cn("lub-form__input", hasError && "lub-form__input--error")}
21
+ aria-invalid={hasError}
22
+ aria-describedby={
23
+ hasError
24
+ ? `${field.name}-error`
25
+ : field.help_text
26
+ ? `${field.name}-help`
27
+ : undefined
28
+ }
29
+ {...register(field.name)}
30
+ />
31
+ </FieldWrapper>
32
+ );
33
+ }
@@ -0,0 +1,33 @@
1
+ import { useFormContext } from "react-hook-form";
2
+ import type { FormField } from "@/api/types";
3
+ import { FieldWrapper } from "./FieldWrapper";
4
+ import { cn } from "@/utils/cn";
5
+
6
+ interface DateTimeFieldProps {
7
+ field: FormField;
8
+ }
9
+
10
+ export function DateTimeField({ field }: DateTimeFieldProps) {
11
+ const { register, formState } = useFormContext();
12
+ const error = formState.errors[field.name];
13
+ const hasError = !!error;
14
+
15
+ return (
16
+ <FieldWrapper field={field}>
17
+ <input
18
+ id={field.name}
19
+ type="datetime-local"
20
+ className={cn("lub-form__input", hasError && "lub-form__input--error")}
21
+ aria-invalid={hasError}
22
+ aria-describedby={
23
+ hasError
24
+ ? `${field.name}-error`
25
+ : field.help_text
26
+ ? `${field.name}-help`
27
+ : undefined
28
+ }
29
+ {...register(field.name)}
30
+ />
31
+ </FieldWrapper>
32
+ );
33
+ }
@@ -0,0 +1,16 @@
1
+ import type { FormField } from "@/api/types";
2
+
3
+ interface DividerFieldProps {
4
+ field: FormField;
5
+ }
6
+
7
+ export function DividerField({ field }: DividerFieldProps) {
8
+ return (
9
+ <div className="lub-form__divider">
10
+ <hr className="lub-form__divider-line" />
11
+ {field.label && (
12
+ <span className="lub-form__divider-label">{field.label}</span>
13
+ )}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from "react";
2
+ import { useFormContext } from "react-hook-form";
3
+ import type { FormField } from "@/api/types";
4
+ import { cn } from "@/utils/cn";
5
+
6
+ interface FieldWrapperProps {
7
+ field: FormField;
8
+ children: ReactNode;
9
+ hideLabel?: boolean;
10
+ }
11
+
12
+ export function FieldWrapper({
13
+ field,
14
+ children,
15
+ hideLabel = false,
16
+ }: FieldWrapperProps) {
17
+ const { formState } = useFormContext();
18
+ const error = formState.errors[field.name];
19
+ const hasError = !!error;
20
+
21
+ return (
22
+ <div
23
+ className={cn(
24
+ "lub-form__field-wrapper",
25
+ hasError && "lub-form__field-wrapper--error",
26
+ )}
27
+ >
28
+ {!hideLabel && field.label && (
29
+ <label
30
+ htmlFor={field.name}
31
+ className={cn(
32
+ "lub-form__label",
33
+ field.required && "lub-form__label--required",
34
+ )}
35
+ >
36
+ {field.label}
37
+ {field.required && (
38
+ <span className="lub-form__required-indicator" aria-hidden="true">
39
+ *
40
+ </span>
41
+ )}
42
+ </label>
43
+ )}
44
+
45
+ {children}
46
+
47
+ {field.help_text && !hasError && (
48
+ <p className="lub-form__help-text" id={`${field.name}-help`}>
49
+ {field.help_text}
50
+ </p>
51
+ )}
52
+
53
+ {hasError && (
54
+ <p className="lub-form__error" id={`${field.name}-error`} role="alert">
55
+ {error.message as string}
56
+ </p>
57
+ )}
58
+ </div>
59
+ );
60
+ }