@schandlergarcia/sf-web-components 1.6.0 → 1.8.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 (162) hide show
  1. package/dist/components/library/cards/ActionList.d.ts +10 -10
  2. package/dist/components/library/cards/ActionList.js +2 -3
  3. package/dist/components/library/cards/ActionList.js.map +1 -1
  4. package/dist/components/library/cards/ActivityCard.d.ts +18 -5
  5. package/dist/components/library/cards/ActivityCard.js +3 -4
  6. package/dist/components/library/cards/ActivityCard.js.map +1 -1
  7. package/dist/components/library/cards/BaseCard.d.ts +30 -24
  8. package/dist/components/library/cards/BaseCard.js +2 -3
  9. package/dist/components/library/cards/BaseCard.js.map +1 -1
  10. package/dist/components/library/cards/CalloutCard.d.ts +11 -9
  11. package/dist/components/library/cards/CalloutCard.js +2 -3
  12. package/dist/components/library/cards/CalloutCard.js.map +1 -1
  13. package/dist/components/library/cards/ChartCard.d.ts +29 -17
  14. package/dist/components/library/cards/ChartCard.js +13 -14
  15. package/dist/components/library/cards/ChartCard.js.map +1 -1
  16. package/dist/components/library/cards/FeedPanel.d.ts +12 -11
  17. package/dist/components/library/cards/FeedPanel.js +3 -4
  18. package/dist/components/library/cards/FeedPanel.js.map +1 -1
  19. package/dist/components/library/cards/ListCard.d.ts +33 -20
  20. package/dist/components/library/cards/ListCard.js +35 -35
  21. package/dist/components/library/cards/ListCard.js.map +1 -1
  22. package/dist/components/library/cards/MetricCard.d.ts +23 -17
  23. package/dist/components/library/cards/MetricCard.js +10 -11
  24. package/dist/components/library/cards/MetricCard.js.map +1 -1
  25. package/dist/components/library/cards/MetricsStrip.d.ts +11 -11
  26. package/dist/components/library/cards/MetricsStrip.js +1 -1
  27. package/dist/components/library/cards/MetricsStrip.js.map +1 -1
  28. package/dist/components/library/cards/SectionCard.d.ts +17 -12
  29. package/dist/components/library/cards/SectionCard.js +18 -19
  30. package/dist/components/library/cards/SectionCard.js.map +1 -1
  31. package/dist/components/library/cards/SemanticMetricCard.d.ts +15 -20
  32. package/dist/components/library/cards/SemanticMetricCardWithLoading.d.ts +8 -7
  33. package/dist/components/library/cards/SemanticTableCard.d.ts +13 -18
  34. package/dist/components/library/cards/SemanticTableCardWithLoading.d.ts +8 -7
  35. package/dist/components/library/cards/StatusCard.d.ts +29 -15
  36. package/dist/components/library/cards/StatusCard.js +16 -17
  37. package/dist/components/library/cards/StatusCard.js.map +1 -1
  38. package/dist/components/library/cards/TableCard.d.ts +40 -23
  39. package/dist/components/library/cards/TableCard.js +59 -59
  40. package/dist/components/library/cards/TableCard.js.map +1 -1
  41. package/dist/components/library/cards/WidgetCard.d.ts +19 -11
  42. package/dist/components/library/cards/WidgetCard.js.map +1 -1
  43. package/dist/components/library/charts/D3Chart.d.ts +23 -16
  44. package/dist/components/library/charts/D3Chart.js.map +1 -1
  45. package/dist/components/library/charts/D3ChartTemplates.d.ts +33 -3
  46. package/dist/components/library/charts/D3ChartTemplates.js +7 -7
  47. package/dist/components/library/charts/D3ChartTemplates.js.map +1 -1
  48. package/dist/components/library/charts/GeoMap.d.ts +81 -18
  49. package/dist/components/library/charts/GeoMap.js +28 -26
  50. package/dist/components/library/charts/GeoMap.js.map +1 -1
  51. package/dist/components/library/filters/FilterBar.d.ts +18 -8
  52. package/dist/components/library/filters/FilterBar.js +2 -3
  53. package/dist/components/library/filters/FilterBar.js.map +1 -1
  54. package/dist/components/library/filters/SearchFilter.d.ts +7 -6
  55. package/dist/components/library/filters/SearchFilter.js +2 -3
  56. package/dist/components/library/filters/SearchFilter.js.map +1 -1
  57. package/dist/components/library/filters/SelectFilter.d.ts +13 -7
  58. package/dist/components/library/filters/SelectFilter.js +2 -3
  59. package/dist/components/library/filters/SelectFilter.js.map +1 -1
  60. package/dist/components/library/filters/ToggleFilter.d.ts +7 -5
  61. package/dist/components/library/filters/ToggleFilter.js +2 -3
  62. package/dist/components/library/filters/ToggleFilter.js.map +1 -1
  63. package/dist/components/library/forms/FormField.d.ts +10 -8
  64. package/dist/components/library/forms/FormField.js +3 -4
  65. package/dist/components/library/forms/FormField.js.map +1 -1
  66. package/dist/components/library/forms/FormModal.d.ts +23 -14
  67. package/dist/components/library/forms/FormModal.js.map +1 -1
  68. package/dist/components/library/forms/FormRenderer.d.ts +29 -9
  69. package/dist/components/library/forms/FormRenderer.js +6 -7
  70. package/dist/components/library/forms/FormRenderer.js.map +1 -1
  71. package/dist/components/library/forms/FormSection.d.ts +10 -8
  72. package/dist/components/library/forms/FormSection.js +2 -3
  73. package/dist/components/library/forms/FormSection.js.map +1 -1
  74. package/dist/components/library/forms/index.d.ts +5 -0
  75. package/dist/components/library/forms/useFormState.d.ts +23 -15
  76. package/dist/components/library/forms/useFormState.js +53 -47
  77. package/dist/components/library/forms/useFormState.js.map +1 -1
  78. package/dist/components/library/heroui/Accordion.d.ts +6 -5
  79. package/dist/components/library/heroui/Accordion.js +7 -8
  80. package/dist/components/library/heroui/Accordion.js.map +1 -1
  81. package/dist/components/library/heroui/Breadcrumbs.d.ts +5 -2
  82. package/dist/components/library/heroui/Breadcrumbs.js +4 -5
  83. package/dist/components/library/heroui/Breadcrumbs.js.map +1 -1
  84. package/dist/components/library/heroui/Collapsible.d.ts +19 -30
  85. package/dist/components/library/heroui/Collapsible.js +13 -13
  86. package/dist/components/library/heroui/Collapsible.js.map +1 -1
  87. package/dist/components/library/heroui/DatePicker.d.ts +24 -52
  88. package/dist/components/library/heroui/DatePicker.js +5 -6
  89. package/dist/components/library/heroui/DatePicker.js.map +1 -1
  90. package/dist/components/library/heroui/Dialog.d.ts +18 -32
  91. package/dist/components/library/heroui/Dialog.js +6 -7
  92. package/dist/components/library/heroui/Dialog.js.map +1 -1
  93. package/dist/components/library/heroui/Drawer.d.ts +6 -2
  94. package/dist/components/library/heroui/Drawer.js +2 -3
  95. package/dist/components/library/heroui/Drawer.js.map +1 -1
  96. package/dist/components/library/heroui/Dropdown.d.ts +6 -2
  97. package/dist/components/library/heroui/Dropdown.js +2 -3
  98. package/dist/components/library/heroui/Dropdown.js.map +1 -1
  99. package/dist/components/library/heroui/Field.d.ts +19 -38
  100. package/dist/components/library/heroui/Field.js +9 -10
  101. package/dist/components/library/heroui/Field.js.map +1 -1
  102. package/dist/components/library/heroui/Meter.d.ts +7 -5
  103. package/dist/components/library/heroui/Meter.js +4 -5
  104. package/dist/components/library/heroui/Meter.js.map +1 -1
  105. package/dist/components/library/heroui/Popover.d.ts +23 -38
  106. package/dist/components/library/heroui/Popover.js +12 -12
  107. package/dist/components/library/heroui/Popover.js.map +1 -1
  108. package/dist/components/library/heroui/Select.d.ts +31 -37
  109. package/dist/components/library/heroui/Select.js +3 -4
  110. package/dist/components/library/heroui/Select.js.map +1 -1
  111. package/dist/components/library/layout/PageContainer.d.ts +6 -4
  112. package/dist/components/library/layout/PageContainer.js +4 -5
  113. package/dist/components/library/layout/PageContainer.js.map +1 -1
  114. package/package.json +4 -1
  115. package/src/components/library/cards/{ActionList.jsx → ActionList.tsx} +13 -9
  116. package/src/components/library/cards/{ActivityCard.jsx → ActivityCard.tsx} +33 -4
  117. package/src/components/library/cards/{BaseCard.jsx → BaseCard.tsx} +33 -6
  118. package/src/components/library/cards/{CalloutCard.jsx → CalloutCard.tsx} +12 -10
  119. package/src/components/library/cards/{ChartCard.jsx → ChartCard.tsx} +32 -6
  120. package/src/components/library/cards/{FeedPanel.jsx → FeedPanel.tsx} +13 -2
  121. package/src/components/library/cards/{ListCard.jsx → ListCard.tsx} +43 -7
  122. package/src/components/library/cards/{MetricCard.jsx → MetricCard.tsx} +25 -6
  123. package/src/components/library/cards/{MetricsStrip.jsx → MetricsStrip.tsx} +22 -12
  124. package/src/components/library/cards/{SectionCard.jsx → SectionCard.tsx} +27 -8
  125. package/src/components/library/cards/{SemanticMetricCard.jsx → SemanticMetricCard.tsx} +17 -5
  126. package/src/components/library/cards/{SemanticMetricCardWithLoading.jsx → SemanticMetricCardWithLoading.tsx} +9 -3
  127. package/src/components/library/cards/{SemanticTableCard.jsx → SemanticTableCard.tsx} +14 -3
  128. package/src/components/library/cards/{SemanticTableCardWithLoading.jsx → SemanticTableCardWithLoading.tsx} +9 -5
  129. package/src/components/library/cards/{StatusCard.jsx → StatusCard.tsx} +61 -12
  130. package/src/components/library/cards/{TableCard.jsx → TableCard.tsx} +51 -12
  131. package/src/components/library/cards/{WidgetCard.jsx → WidgetCard.tsx} +28 -5
  132. package/src/components/library/charts/{D3Chart.jsx → D3Chart.tsx} +27 -7
  133. package/src/components/library/charts/{D3ChartTemplates.jsx → D3ChartTemplates.tsx} +60 -28
  134. package/src/components/library/charts/{GeoMap.jsx → GeoMap.tsx} +106 -17
  135. package/src/components/library/filters/{FilterBar.jsx → FilterBar.tsx} +21 -11
  136. package/src/components/library/filters/{SearchFilter.jsx → SearchFilter.tsx} +8 -2
  137. package/src/components/library/filters/{SelectFilter.jsx → SelectFilter.tsx} +15 -8
  138. package/src/components/library/filters/{ToggleFilter.jsx → ToggleFilter.tsx} +7 -6
  139. package/src/components/library/forms/{FormField.jsx → FormField.tsx} +91 -45
  140. package/src/components/library/forms/{FormModal.jsx → FormModal.tsx} +21 -20
  141. package/src/components/library/forms/{FormRenderer.jsx → FormRenderer.tsx} +32 -10
  142. package/src/components/library/forms/{FormSection.jsx → FormSection.tsx} +13 -7
  143. package/src/components/library/forms/index.tsx +11 -0
  144. package/src/components/library/forms/{useFormState.jsx → useFormState.tsx} +43 -23
  145. package/src/components/library/heroui/{Accordion.jsx → Accordion.tsx} +8 -3
  146. package/src/components/library/heroui/{Breadcrumbs.jsx → Breadcrumbs.tsx} +5 -2
  147. package/src/components/library/heroui/Collapsible.tsx +62 -0
  148. package/src/components/library/heroui/{DatePicker.jsx → DatePicker.tsx} +28 -4
  149. package/src/components/library/heroui/Dialog.tsx +43 -0
  150. package/src/components/library/heroui/{Drawer.jsx → Drawer.tsx} +6 -2
  151. package/src/components/library/heroui/{Dropdown.jsx → Dropdown.tsx} +6 -2
  152. package/src/components/library/heroui/{Field.jsx → Field.tsx} +23 -6
  153. package/src/components/library/heroui/Meter.tsx +13 -0
  154. package/src/components/library/heroui/{Popover.jsx → Popover.tsx} +29 -8
  155. package/src/components/library/heroui/Select.tsx +73 -0
  156. package/src/components/library/layout/{PageContainer.jsx → PageContainer.tsx} +6 -3
  157. package/src/components/library/forms/index.jsx +0 -5
  158. package/src/components/library/heroui/Collapsible.jsx +0 -42
  159. package/src/components/library/heroui/Dialog.jsx +0 -37
  160. package/src/components/library/heroui/Meter.jsx +0 -8
  161. package/src/components/library/heroui/Select.jsx +0 -37
  162. /package/src/components/library/filters/{index.jsx → index.ts} +0 -0
@@ -1,18 +1,19 @@
1
- import React from "react";
1
+ export interface ToggleFilterProps {
2
+ value?: boolean;
3
+ onChange?: (value: boolean) => void;
4
+ label?: string;
5
+ className?: string;
6
+ }
2
7
 
3
8
  /**
4
9
  * Toggle switch filter.
5
- *
6
- * @param {boolean} value — current on/off state
7
- * @param {Function} onChange — (boolean) => void
8
- * @param {string} label — visible label
9
10
  */
10
11
  export default function ToggleFilter({
11
12
  value = false,
12
13
  onChange,
13
14
  label,
14
15
  className = "",
15
- }) {
16
+ }: ToggleFilterProps) {
16
17
  return (
17
18
  <label
18
19
  className={[
@@ -1,5 +1,6 @@
1
- import React from "react";
1
+ import * as React from "react";
2
2
  import { ChevronDownIcon } from "@heroicons/react/24/outline";
3
+ import type { FormField as FormFieldType } from "./FormRenderer";
3
4
 
4
5
  const INPUT_BASE =
5
6
  "h-10 w-full rounded-lg border border-slate-200 bg-white px-3 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus:ring-offset-slate-950";
@@ -7,11 +8,17 @@ const INPUT_BASE =
7
8
  const INPUT_ERROR =
8
9
  "border-red-300 focus:ring-red-500 dark:border-red-700 dark:focus:ring-red-500";
9
10
 
10
- function cx(...classes) {
11
+ function cx(...classes: (string | boolean | undefined)[]): string {
11
12
  return classes.filter(Boolean).join(" ");
12
13
  }
13
14
 
14
- function FieldLabel({ label, required, htmlFor }) {
15
+ interface FieldLabelProps {
16
+ label?: string;
17
+ required?: boolean;
18
+ htmlFor: string;
19
+ }
20
+
21
+ function FieldLabel({ label, required, htmlFor }: FieldLabelProps) {
15
22
  if (!label) return null;
16
23
  return (
17
24
  <label htmlFor={htmlFor} className="block text-sm font-medium text-slate-700 dark:text-slate-200">
@@ -21,20 +28,36 @@ function FieldLabel({ label, required, htmlFor }) {
21
28
  );
22
29
  }
23
30
 
24
- function FieldError({ error }) {
31
+ interface FieldErrorProps {
32
+ error?: string;
33
+ }
34
+
35
+ function FieldError({ error }: FieldErrorProps) {
25
36
  if (!error) return null;
26
37
  return <p className="text-xs text-red-600 dark:text-red-400">{error}</p>;
27
38
  }
28
39
 
29
- function FieldDescription({ description }) {
40
+ interface FieldDescriptionProps {
41
+ description?: string;
42
+ }
43
+
44
+ function FieldDescription({ description }: FieldDescriptionProps) {
30
45
  if (!description) return null;
31
46
  return <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>;
32
47
  }
33
48
 
34
49
  // ─── Individual field renderers ───
35
50
 
36
- function TextField({ field, value, onChange, onBlur, error }) {
37
- const inputType = field.inputType ?? field.type;
51
+ interface FieldRendererProps {
52
+ field: FormFieldType & Record<string, unknown>;
53
+ value: unknown;
54
+ onChange: (value: unknown) => void;
55
+ onBlur: () => void;
56
+ error?: boolean;
57
+ }
58
+
59
+ function TextField({ field, value, onChange, onBlur, error }: FieldRendererProps) {
60
+ const inputType = (field.inputType as string | undefined) ?? field.type;
38
61
  const type = { text: "text", email: "email", url: "url", number: "number", date: "date" }[inputType] ?? "text";
39
62
 
40
63
  return (
@@ -42,32 +65,32 @@ function TextField({ field, value, onChange, onBlur, error }) {
42
65
  id={field.id}
43
66
  name={field.id}
44
67
  type={type}
45
- value={value ?? ""}
68
+ value={(value as string | number) ?? ""}
46
69
  onChange={(e) => onChange(field.type === "number" ? e.target.value : e.target.value)}
47
70
  onBlur={onBlur}
48
- placeholder={field.placeholder}
49
- disabled={field.disabled}
50
- readOnly={field.readOnly}
51
- min={field.min}
52
- max={field.max}
53
- step={field.step}
71
+ placeholder={field.placeholder as string | undefined}
72
+ disabled={field.disabled as boolean | undefined}
73
+ readOnly={field.readOnly as boolean | undefined}
74
+ min={field.min as number | undefined}
75
+ max={field.max as number | undefined}
76
+ step={field.step as number | undefined}
54
77
  className={cx(INPUT_BASE, error && INPUT_ERROR)}
55
78
  />
56
79
  );
57
80
  }
58
81
 
59
- function TextareaField({ field, value, onChange, onBlur, error }) {
82
+ function TextareaField({ field, value, onChange, onBlur, error }: FieldRendererProps) {
60
83
  return (
61
84
  <textarea
62
85
  id={field.id}
63
86
  name={field.id}
64
- value={value ?? ""}
87
+ value={(value as string) ?? ""}
65
88
  onChange={(e) => onChange(e.target.value)}
66
89
  onBlur={onBlur}
67
- placeholder={field.placeholder}
68
- disabled={field.disabled}
69
- readOnly={field.readOnly}
70
- rows={field.rows ?? 3}
90
+ placeholder={field.placeholder as string | undefined}
91
+ disabled={field.disabled as boolean | undefined}
92
+ readOnly={field.readOnly as boolean | undefined}
93
+ rows={(field.rows as number | undefined) ?? 3}
71
94
  className={cx(
72
95
  "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus:ring-offset-slate-950",
73
96
  error && INPUT_ERROR
@@ -76,8 +99,13 @@ function TextareaField({ field, value, onChange, onBlur, error }) {
76
99
  );
77
100
  }
78
101
 
79
- function SelectField({ field, value, onChange, onBlur, error }) {
80
- const options = (field.options ?? []).map((opt) =>
102
+ interface SelectOption {
103
+ value: string;
104
+ label: string;
105
+ }
106
+
107
+ function SelectField({ field, value, onChange, onBlur, error }: FieldRendererProps) {
108
+ const options = ((field.options as (string | SelectOption)[] | undefined) ?? []).map((opt) =>
81
109
  typeof opt === "string" ? { value: opt, label: opt } : opt
82
110
  );
83
111
 
@@ -86,17 +114,17 @@ function SelectField({ field, value, onChange, onBlur, error }) {
86
114
  <select
87
115
  id={field.id}
88
116
  name={field.id}
89
- value={value ?? ""}
117
+ value={(value as string) ?? ""}
90
118
  onChange={(e) => onChange(e.target.value)}
91
119
  onBlur={onBlur}
92
- disabled={field.disabled}
120
+ disabled={field.disabled as boolean | undefined}
93
121
  className={cx(
94
122
  "h-10 w-full appearance-none rounded-lg border border-slate-200 bg-white py-0 pl-3 pr-9 text-sm font-medium text-slate-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200 dark:focus:ring-offset-slate-950",
95
123
  error && INPUT_ERROR
96
124
  )}
97
125
  >
98
126
  {field.placeholder ? (
99
- <option value="">{field.placeholder}</option>
127
+ <option value="">{field.placeholder as string}</option>
100
128
  ) : null}
101
129
  {options.map((opt) => (
102
130
  <option key={opt.value} value={opt.value}>
@@ -112,11 +140,16 @@ function SelectField({ field, value, onChange, onBlur, error }) {
112
140
  );
113
141
  }
114
142
 
115
- function RadioField({ field, value, onChange }) {
116
- const options = (field.options ?? []).map((opt) =>
143
+ interface RadioOption extends SelectOption {
144
+ description?: string;
145
+ disabled?: boolean;
146
+ }
147
+
148
+ function RadioField({ field, value, onChange }: FieldRendererProps) {
149
+ const options = ((field.options as (string | RadioOption)[] | undefined) ?? []).map((opt) =>
117
150
  typeof opt === "string" ? { value: opt, label: opt } : opt
118
151
  );
119
- const layout = field.layout ?? (options.length <= 4 ? "horizontal" : "vertical");
152
+ const layout = (field.layout as string | undefined) ?? (options.length <= 4 ? "horizontal" : "vertical");
120
153
 
121
154
  return (
122
155
  <div
@@ -138,7 +171,7 @@ function RadioField({ field, value, onChange }) {
138
171
  value={opt.value}
139
172
  checked={value === opt.value}
140
173
  onChange={() => onChange(opt.value)}
141
- disabled={field.disabled || opt.disabled}
174
+ disabled={(field.disabled as boolean | undefined) || opt.disabled}
142
175
  className="h-4 w-4 border-slate-300 text-brand-600 focus:ring-brand-500 dark:border-slate-600 dark:bg-slate-800"
143
176
  />
144
177
  {opt.label}
@@ -151,7 +184,7 @@ function RadioField({ field, value, onChange }) {
151
184
  );
152
185
  }
153
186
 
154
- function CheckboxField({ field, value, onChange }) {
187
+ function CheckboxField({ field, value, onChange }: FieldRendererProps) {
155
188
  return (
156
189
  <label className="inline-flex cursor-pointer items-center gap-2.5 text-sm text-slate-700 dark:text-slate-200">
157
190
  <input
@@ -160,22 +193,26 @@ function CheckboxField({ field, value, onChange }) {
160
193
  name={field.id}
161
194
  checked={Boolean(value)}
162
195
  onChange={(e) => onChange(e.target.checked)}
163
- disabled={field.disabled}
196
+ disabled={field.disabled as boolean | undefined}
164
197
  className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500 dark:border-slate-600 dark:bg-slate-800"
165
198
  />
166
- {field.checkboxLabel ?? field.label}
199
+ {(field.checkboxLabel as string | undefined) ?? field.label}
167
200
  </label>
168
201
  );
169
202
  }
170
203
 
171
- function CheckboxGroupField({ field, value, onChange }) {
204
+ interface CheckboxOption extends SelectOption {
205
+ disabled?: boolean;
206
+ }
207
+
208
+ function CheckboxGroupField({ field, value, onChange }: FieldRendererProps) {
172
209
  const selected = Array.isArray(value) ? value : [];
173
- const options = (field.options ?? []).map((opt) =>
210
+ const options = ((field.options as (string | CheckboxOption)[] | undefined) ?? []).map((opt) =>
174
211
  typeof opt === "string" ? { value: opt, label: opt } : opt
175
212
  );
176
- const layout = field.layout ?? (options.length <= 4 ? "horizontal" : "vertical");
213
+ const layout = (field.layout as string | undefined) ?? (options.length <= 4 ? "horizontal" : "vertical");
177
214
 
178
- function toggleValue(optValue) {
215
+ function toggleValue(optValue: string) {
179
216
  const next = selected.includes(optValue)
180
217
  ? selected.filter((v) => v !== optValue)
181
218
  : [...selected, optValue];
@@ -198,7 +235,7 @@ function CheckboxGroupField({ field, value, onChange }) {
198
235
  type="checkbox"
199
236
  checked={selected.includes(opt.value)}
200
237
  onChange={() => toggleValue(opt.value)}
201
- disabled={field.disabled || opt.disabled}
238
+ disabled={(field.disabled as boolean | undefined) || opt.disabled}
202
239
  className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500 dark:border-slate-600 dark:bg-slate-800"
203
240
  />
204
241
  {opt.label}
@@ -208,7 +245,7 @@ function CheckboxGroupField({ field, value, onChange }) {
208
245
  );
209
246
  }
210
247
 
211
- function ToggleField({ field, value, onChange }) {
248
+ function ToggleField({ field, value, onChange }: FieldRendererProps) {
212
249
  const checked = Boolean(value);
213
250
 
214
251
  return (
@@ -218,7 +255,7 @@ function ToggleField({ field, value, onChange }) {
218
255
  role="switch"
219
256
  aria-checked={checked}
220
257
  onClick={() => onChange(!checked)}
221
- disabled={field.disabled}
258
+ disabled={field.disabled as boolean | undefined}
222
259
  className={cx(
223
260
  "relative inline-flex h-6 w-11 shrink-0 rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:focus:ring-offset-slate-950",
224
261
  checked ? "bg-brand-500" : "bg-slate-200 dark:bg-slate-700",
@@ -234,7 +271,7 @@ function ToggleField({ field, value, onChange }) {
234
271
  />
235
272
  </button>
236
273
  {field.toggleLabel ? (
237
- <span className="text-sm text-slate-700 dark:text-slate-200">{field.toggleLabel}</span>
274
+ <span className="text-sm text-slate-700 dark:text-slate-200">{field.toggleLabel as string}</span>
238
275
  ) : null}
239
276
  </div>
240
277
  );
@@ -242,7 +279,7 @@ function ToggleField({ field, value, onChange }) {
242
279
 
243
280
  // ─── Main FormField ───
244
281
 
245
- const FIELD_RENDERERS = {
282
+ const FIELD_RENDERERS: Record<string, React.ComponentType<FieldRendererProps>> = {
246
283
  text: TextField,
247
284
  email: TextField,
248
285
  url: TextField,
@@ -256,11 +293,20 @@ const FIELD_RENDERERS = {
256
293
  toggle: ToggleField,
257
294
  };
258
295
 
296
+ export interface FormFieldProps {
297
+ field: FormFieldType & Record<string, unknown>;
298
+ value: unknown;
299
+ error?: string;
300
+ touched?: boolean;
301
+ onChange: (value: unknown) => void;
302
+ onBlur: () => void;
303
+ }
304
+
259
305
  /**
260
306
  * Renders a single form field with label, description, error message,
261
307
  * and the appropriate input type.
262
308
  */
263
- export default function FormField({ field, value, error, touched, onChange, onBlur }) {
309
+ export default function FormField({ field, value, error, touched, onChange, onBlur }: FormFieldProps) {
264
310
  const Renderer = FIELD_RENDERERS[field.type];
265
311
  if (!Renderer) return null;
266
312
 
@@ -273,17 +319,17 @@ export default function FormField({ field, value, error, touched, onChange, onBl
273
319
  <FieldLabel label={field.label} required={field.required} htmlFor={field.id} />
274
320
  ) : null}
275
321
  {field.description && field.type !== "toggle" ? (
276
- <FieldDescription description={field.description} />
322
+ <FieldDescription description={field.description as string} />
277
323
  ) : null}
278
324
  <Renderer
279
325
  field={field}
280
326
  value={value}
281
327
  onChange={onChange}
282
328
  onBlur={onBlur}
283
- error={showError}
329
+ error={Boolean(showError)}
284
330
  />
285
331
  {field.type === "toggle" && field.description ? (
286
- <FieldDescription description={field.description} />
332
+ <FieldDescription description={field.description as string} />
287
333
  ) : null}
288
334
  {showError ? <FieldError error={error} /> : null}
289
335
  </div>
@@ -1,10 +1,11 @@
1
- import React, { useEffect, useCallback } from "react";
1
+ import { useEffect, useCallback } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { motion, AnimatePresence } from "framer-motion";
4
4
  import { XMarkIcon } from "@heroicons/react/24/outline";
5
5
  import Spinner from "../ui/Spinner";
6
6
  import FormRenderer from "./FormRenderer";
7
7
  import useFormState from "./useFormState";
8
+ import type { FormSection } from "./FormRenderer";
8
9
 
9
10
  const OVERLAY_VARIANTS = {
10
11
  hidden: { opacity: 0 },
@@ -13,35 +14,35 @@ const OVERLAY_VARIANTS = {
13
14
 
14
15
  const PANEL_VARIANTS = {
15
16
  hidden: { opacity: 0, y: 24, scale: 0.97 },
16
- visible: { opacity: 1, y: 0, scale: 1, transition: { type: "spring", damping: 25, stiffness: 350 } },
17
+ visible: { opacity: 1, y: 0, scale: 1, transition: { type: "spring" as const, damping: 25, stiffness: 350 } },
17
18
  exit: { opacity: 0, y: 16, scale: 0.97, transition: { duration: 0.15 } },
18
19
  };
19
20
 
20
- /**
21
- * Size → max-width mapping.
22
- */
23
21
  const SIZE_CLASSES = {
24
22
  sm: "max-w-md",
25
23
  md: "max-w-xl",
26
24
  lg: "max-w-2xl",
27
25
  xl: "max-w-4xl",
28
- };
26
+ } as const;
27
+
28
+ export interface FormModalProps {
29
+ isOpen?: boolean;
30
+ onClose?: () => void;
31
+ title: string;
32
+ subtitle?: string;
33
+ sections?: FormSection[];
34
+ initialValues?: Record<string, unknown>;
35
+ onSubmit?: (values: Record<string, unknown>) => void | Promise<void>;
36
+ submitLabel?: string;
37
+ cancelLabel?: string;
38
+ size?: keyof typeof SIZE_CLASSES;
39
+ destructive?: boolean;
40
+ minSubmitMs?: number;
41
+ }
29
42
 
30
43
  /**
31
44
  * Modal dialog for creating or editing records.
32
45
  * Wraps FormRenderer + useFormState in an animated overlay.
33
- *
34
- * @param {boolean} isOpen — whether the modal is visible
35
- * @param {Function} onClose — close handler
36
- * @param {string} title — modal title (e.g. "Edit Service", "New Incident")
37
- * @param {string} subtitle — optional subtitle
38
- * @param {Array} sections — form schema sections
39
- * @param {Object} initialValues — prefill for editing (empty = create mode)
40
- * @param {Function} onSubmit — async (values) => void, called on valid submit
41
- * @param {string} submitLabel — submit button text (default: "Save")
42
- * @param {string} cancelLabel — cancel button text (default: "Cancel")
43
- * @param {string} size — "sm" | "md" | "lg" | "xl" (default: "lg")
44
- * @param {boolean} destructive — if true, submit button is red (for delete confirmations)
45
46
  */
46
47
  export default function FormModal({
47
48
  isOpen = false,
@@ -56,7 +57,7 @@ export default function FormModal({
56
57
  size = "lg",
57
58
  destructive = false,
58
59
  minSubmitMs,
59
- }) {
60
+ }: FormModalProps) {
60
61
  const form = useFormState({
61
62
  initialValues,
62
63
  sections,
@@ -73,7 +74,7 @@ export default function FormModal({
73
74
  }, [isOpen]);
74
75
 
75
76
  const onKeyDown = useCallback(
76
- (e) => {
77
+ (e: KeyboardEvent) => {
77
78
  if (e.key === "Escape") onClose?.();
78
79
  },
79
80
  [onClose]
@@ -1,17 +1,39 @@
1
- import React from "react";
2
1
  import FormSection from "./FormSection";
3
2
 
3
+ export interface FormSection {
4
+ id?: string;
5
+ title?: string;
6
+ description?: string;
7
+ fields: FormField[];
8
+ }
9
+
10
+ export interface FormField {
11
+ id: string;
12
+ type: string;
13
+ label?: string;
14
+ placeholder?: string;
15
+ description?: string;
16
+ required?: boolean;
17
+ requiredMessage?: string;
18
+ disabled?: boolean;
19
+ readOnly?: boolean;
20
+ validate?: (value: unknown, values: Record<string, unknown>) => string | undefined;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface FormRendererProps {
25
+ sections?: FormSection[];
26
+ values?: Record<string, unknown>;
27
+ errors?: Record<string, string>;
28
+ touched?: Record<string, boolean>;
29
+ onFieldChange: (fieldId: string, value: unknown) => void;
30
+ onFieldBlur: (fieldId: string) => void;
31
+ formError?: string;
32
+ }
33
+
4
34
  /**
5
35
  * Renders a complete form from a schema definition.
6
36
  * Pairs with useFormState for state management.
7
- *
8
- * @param {Array} sections — form schema sections
9
- * @param {Object} values — current form values
10
- * @param {Object} errors — current validation errors
11
- * @param {Object} touched — which fields have been touched
12
- * @param {Function} onFieldChange — (fieldId, value) => void
13
- * @param {Function} onFieldBlur — (fieldId) => void
14
- * @param {string} formError — top-level form error (e.g. submission failure)
15
37
  */
16
38
  export default function FormRenderer({
17
39
  sections = [],
@@ -21,7 +43,7 @@ export default function FormRenderer({
21
43
  onFieldChange,
22
44
  onFieldBlur,
23
45
  formError,
24
- }) {
46
+ }: FormRendererProps) {
25
47
  return (
26
48
  <div className="space-y-8">
27
49
  {formError ? (
@@ -1,15 +1,21 @@
1
- import React from "react";
2
1
  import FormField from "./FormField";
2
+ import type { FormSection as FormSectionType, FormField as FormFieldType } from "./FormRenderer";
3
3
 
4
- /**
5
- * Fields that naturally span full width by default.
6
- */
7
4
  const FULL_WIDTH_TYPES = new Set([
8
5
  "textarea",
9
6
  "radio",
10
7
  "checkboxGroup",
11
8
  ]);
12
9
 
10
+ export interface FormSectionProps {
11
+ section: FormSectionType;
12
+ values: Record<string, unknown>;
13
+ errors: Record<string, string>;
14
+ touched: Record<string, boolean>;
15
+ onFieldChange: (fieldId: string, value: unknown) => void;
16
+ onFieldBlur: (fieldId: string) => void;
17
+ }
18
+
13
19
  /**
14
20
  * A titled section of a form with a 2-column grid layout.
15
21
  * Fields default to 1-column (half width) unless the type is naturally
@@ -22,7 +28,7 @@ export default function FormSection({
22
28
  touched,
23
29
  onFieldChange,
24
30
  onFieldBlur,
25
- }) {
31
+ }: FormSectionProps) {
26
32
  const fields = section.fields ?? [];
27
33
  if (!fields.length) return null;
28
34
 
@@ -42,7 +48,7 @@ export default function FormSection({
42
48
  ) : null}
43
49
 
44
50
  <div className="grid grid-cols-1 gap-x-4 gap-y-5 sm:grid-cols-2">
45
- {fields.map((field) => {
51
+ {fields.map((field: FormFieldType & { colSpan?: number }) => {
46
52
  const span =
47
53
  field.colSpan ??
48
54
  (FULL_WIDTH_TYPES.has(field.type) ? 2 : 1);
@@ -57,7 +63,7 @@ export default function FormSection({
57
63
  value={values[field.id]}
58
64
  error={errors[field.id]}
59
65
  touched={touched[field.id]}
60
- onChange={(val) => onFieldChange(field.id, val)}
66
+ onChange={(val: unknown) => onFieldChange(field.id, val)}
61
67
  onBlur={() => onFieldBlur(field.id)}
62
68
  />
63
69
  </div>
@@ -0,0 +1,11 @@
1
+ export { default as FormModal } from "./FormModal";
2
+ export { default as FormRenderer } from "./FormRenderer";
3
+ export { default as FormSection } from "./FormSection";
4
+ export { default as FormField } from "./FormField";
5
+ export { default as useFormState } from "./useFormState";
6
+
7
+ export type { FormModalProps } from "./FormModal";
8
+ export type { FormRendererProps } from "./FormRenderer";
9
+ export type { FormSection as FormSectionType, FormField as FormFieldType } from "./FormRenderer";
10
+ export type { FormSectionProps } from "./FormSection";
11
+ export type { FormFieldProps } from "./FormField";
@@ -1,12 +1,35 @@
1
1
  import { useState, useMemo, useCallback, useRef } from "react";
2
+ import type { FormSection } from "./FormRenderer";
2
3
 
3
4
  const DEFAULT_MIN_SUBMIT_MS = 4000;
4
5
 
6
+ interface UseFormStateOptions {
7
+ initialValues?: Record<string, unknown>;
8
+ sections?: FormSection[];
9
+ onSubmit?: (values: Record<string, unknown>) => void | Promise<void>;
10
+ minSubmitMs?: number;
11
+ }
12
+
13
+ interface UseFormStateReturn {
14
+ values: Record<string, unknown>;
15
+ errors: Record<string, string>;
16
+ touched: Record<string, boolean>;
17
+ isDirty: boolean;
18
+ isValid: boolean;
19
+ isSubmitting: boolean;
20
+ setValue: (id: string, value: unknown) => void;
21
+ setValues: (vals: Record<string, unknown>) => void;
22
+ setTouched: (id: string) => void;
23
+ validate: () => boolean;
24
+ reset: () => void;
25
+ handleSubmit: (e?: React.FormEvent) => Promise<boolean>;
26
+ }
27
+
5
28
  /**
6
29
  * Extracts all field ids and their default values from a form schema.
7
30
  */
8
- function buildDefaults(sections) {
9
- const defaults = {};
31
+ function buildDefaults(sections: FormSection[]): Record<string, unknown> {
32
+ const defaults: Record<string, unknown> = {};
10
33
  for (const section of sections) {
11
34
  for (const field of section.fields ?? []) {
12
35
  if (field.type === "checkboxGroup") {
@@ -25,8 +48,8 @@ function buildDefaults(sections) {
25
48
  * Runs validation for all fields.
26
49
  * Returns an object of { fieldId: errorMessage } (empty object = valid).
27
50
  */
28
- function runValidation(values, sections) {
29
- const errors = {};
51
+ function runValidation(values: Record<string, unknown>, sections: FormSection[]): Record<string, string> {
52
+ const errors: Record<string, string> = {};
30
53
  for (const section of sections) {
31
54
  for (const field of section.fields ?? []) {
32
55
  const val = values[field.id];
@@ -54,23 +77,19 @@ function runValidation(values, sections) {
54
77
 
55
78
  /**
56
79
  * Form state management hook.
57
- *
58
- * @param {Object} options
59
- * @param {Object} options.initialValues — prefill for editing (merged over field defaults)
60
- * @param {Array} options.sections — form schema sections (used for defaults + validation)
61
- * @param {Function} options.onSubmit — called with (values) when form is valid
62
- * @param {number} options.minSubmitMs — minimum time the submit spinner shows (default 4000ms, set 0 to disable)
63
- *
64
- * @returns {Object} { values, errors, touched, isDirty, isValid, isSubmitting,
65
- * setValue, setValues, setTouched, validate, reset, handleSubmit }
66
80
  */
67
- export default function useFormState({ initialValues = {}, sections = [], onSubmit, minSubmitMs = DEFAULT_MIN_SUBMIT_MS } = {}) {
81
+ export default function useFormState({
82
+ initialValues = {},
83
+ sections = [],
84
+ onSubmit,
85
+ minSubmitMs = DEFAULT_MIN_SUBMIT_MS
86
+ }: UseFormStateOptions = {}): UseFormStateReturn {
68
87
  const defaults = useMemo(() => buildDefaults(sections), [sections]);
69
88
  const merged = useMemo(() => ({ ...defaults, ...initialValues }), [defaults, initialValues]);
70
89
 
71
- const [values, setValuesState] = useState(merged);
72
- const [errors, setErrors] = useState({});
73
- const [touched, setTouchedState] = useState({});
90
+ const [values, setValuesState] = useState<Record<string, unknown>>(merged);
91
+ const [errors, setErrors] = useState<Record<string, string>>({});
92
+ const [touched, setTouchedState] = useState<Record<string, boolean>>({});
74
93
  const [isSubmitting, setIsSubmitting] = useState(false);
75
94
  const submitRef = useRef(onSubmit);
76
95
  submitRef.current = onSubmit;
@@ -86,7 +105,7 @@ export default function useFormState({ initialValues = {}, sections = [], onSubm
86
105
  });
87
106
  }, [merged, values]);
88
107
 
89
- const setValue = useCallback((id, value) => {
108
+ const setValue = useCallback((id: string, value: unknown) => {
90
109
  setValuesState((prev) => ({ ...prev, [id]: value }));
91
110
  setErrors((prev) => {
92
111
  if (!prev[id]) return prev;
@@ -96,18 +115,18 @@ export default function useFormState({ initialValues = {}, sections = [], onSubm
96
115
  });
97
116
  }, []);
98
117
 
99
- const setValues = useCallback((vals) => {
118
+ const setValues = useCallback((vals: Record<string, unknown>) => {
100
119
  setValuesState((prev) => ({ ...prev, ...vals }));
101
120
  }, []);
102
121
 
103
- const setTouched = useCallback((id) => {
122
+ const setTouched = useCallback((id: string) => {
104
123
  setTouchedState((prev) => (prev[id] ? prev : { ...prev, [id]: true }));
105
124
  }, []);
106
125
 
107
126
  const validate = useCallback(() => {
108
127
  const errs = runValidation(values, sections);
109
128
  setErrors(errs);
110
- const allTouched = {};
129
+ const allTouched: Record<string, boolean> = {};
111
130
  for (const section of sections) {
112
131
  for (const field of section.fields ?? []) {
113
132
  allTouched[field.id] = true;
@@ -124,7 +143,7 @@ export default function useFormState({ initialValues = {}, sections = [], onSubm
124
143
  }, [merged]);
125
144
 
126
145
  const handleSubmit = useCallback(
127
- async (e) => {
146
+ async (e?: React.FormEvent) => {
128
147
  e?.preventDefault?.();
129
148
  const valid = validate();
130
149
  if (!valid) return false;
@@ -137,7 +156,8 @@ export default function useFormState({ initialValues = {}, sections = [], onSubm
137
156
  await Promise.all([submitRef.current?.(values), delay]);
138
157
  return true;
139
158
  } catch (err) {
140
- setErrors((prev) => ({ ...prev, _form: err?.message ?? "Submission failed" }));
159
+ const errorMessage = err instanceof Error ? err.message : "Submission failed";
160
+ setErrors((prev) => ({ ...prev, _form: errorMessage }));
141
161
  return false;
142
162
  } finally {
143
163
  setIsSubmitting(false);