@visns-studio/visns-components 5.8.11 → 5.8.13
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/package.json +12 -12
- package/src/components/crm/Autocomplete.jsx +211 -49
- package/src/components/crm/Field.jsx +28 -1
- package/src/components/crm/Form.jsx +20 -7
- package/src/components/crm/SectionHeader.jsx +29 -0
- package/src/components/crm/examples/FormStylingGuide.md +71 -0
- package/src/components/crm/examples/FormWithSectionHeaders.jsx +266 -0
- package/src/components/crm/generic/ConfirmationDialog.jsx +97 -0
- package/src/components/crm/generic/GenericIndex.jsx +2 -3
- package/src/components/crm/generic/GenericQuote.jsx +413 -120
- package/src/components/crm/generic/SortableQuoteItems.jsx +114 -70
- package/src/components/crm/styles/Autocomplete.module.scss +97 -13
- package/src/components/crm/styles/Field.module.scss +88 -5
package/package.json
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"@fontsource/barlow": "^5.2.5",
|
|
5
5
|
"@inovua/reactdatagrid-community": "^5.10.2",
|
|
6
6
|
"@inovua/reactdatagrid-enterprise": "^5.10.2",
|
|
7
|
-
"@nivo/bar": "^0.
|
|
8
|
-
"@nivo/core": "^0.
|
|
9
|
-
"@nivo/line": "^0.
|
|
10
|
-
"@nivo/pie": "^0.
|
|
7
|
+
"@nivo/bar": "^0.98.0",
|
|
8
|
+
"@nivo/core": "^0.98.0",
|
|
9
|
+
"@nivo/line": "^0.98.0",
|
|
10
|
+
"@nivo/pie": "^0.98.0",
|
|
11
11
|
"@tinymce/tinymce-react": "^6.1.0",
|
|
12
12
|
"@visns-studio/visns-datagrid-community": "^1.0.14",
|
|
13
13
|
"@visns-studio/visns-datagrid-enterprise": "^1.0.14",
|
|
14
|
-
"@vitejs/plugin-react": "^4.
|
|
14
|
+
"@vitejs/plugin-react": "^4.5.0",
|
|
15
15
|
"add": "^2.0.6",
|
|
16
16
|
"akar-icons": "^1.9.31",
|
|
17
17
|
"array-move": "^4.0.0",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"dayjs": "^1.11.13",
|
|
21
21
|
"fabric": "^6.6.5",
|
|
22
22
|
"file-saver": "^2.0.5",
|
|
23
|
-
"framer-motion": "^12.
|
|
23
|
+
"framer-motion": "^12.12.1",
|
|
24
24
|
"html-react-parser": "^5.2.5",
|
|
25
25
|
"lodash": "^4.17.21",
|
|
26
26
|
"lodash.debounce": "^4.0.8",
|
|
27
27
|
"moment": "^2.30.1",
|
|
28
|
-
"motion": "^12.
|
|
28
|
+
"motion": "^12.12.1",
|
|
29
29
|
"numeral": "^2.0.6",
|
|
30
30
|
"pluralize": "^8.0.0",
|
|
31
31
|
"qrcode.react": "^4.2.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"react-promise-tracker": "^2.1.1",
|
|
42
42
|
"react-quill": "^2.0.0",
|
|
43
43
|
"react-reveal": "^1.2.2",
|
|
44
|
-
"react-router-dom": "^6.30.
|
|
44
|
+
"react-router-dom": "^6.30.1",
|
|
45
45
|
"react-select": "^5.10.1",
|
|
46
46
|
"react-signature-pad-wrapper": "^4.1.0",
|
|
47
47
|
"react-slugify": "^4.0.1",
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"reactjs-popup": "^2.0.6",
|
|
55
55
|
"style-loader": "^4.0.0",
|
|
56
56
|
"swapy": "^1.0.5",
|
|
57
|
-
"sweetalert2": "^11.21.
|
|
57
|
+
"sweetalert2": "^11.21.2",
|
|
58
58
|
"truncate": "^3.0.0",
|
|
59
59
|
"uuid": "^11.1.0",
|
|
60
60
|
"validator": "^13.15.0",
|
|
61
61
|
"vite": "^6.3.5",
|
|
62
62
|
"yarn": "^1.22.22",
|
|
63
|
-
"yet-another-react-lightbox": "^3.23.
|
|
63
|
+
"yet-another-react-lightbox": "^3.23.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@babel/core": "^7.27.1",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"mini-css-extract-plugin": "^2.9.2",
|
|
75
75
|
"react": "^18.3.1",
|
|
76
76
|
"react-dom": "^18.3.1",
|
|
77
|
-
"sass": "^1.
|
|
77
|
+
"sass": "^1.89.0",
|
|
78
78
|
"sass-loader": "^16.0.5"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
83
83
|
},
|
|
84
84
|
"name": "@visns-studio/visns-components",
|
|
85
|
-
"version": "5.8.
|
|
85
|
+
"version": "5.8.13",
|
|
86
86
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
87
87
|
"main": "src/index.js",
|
|
88
88
|
"files": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, memo, useRef } from 'react';
|
|
2
2
|
import AwesomeDebouncePromise from 'awesome-debounce-promise';
|
|
3
|
-
import { ToggleOffFill, ToggleOnFill } from 'akar-icons';
|
|
3
|
+
import { ToggleOffFill, ToggleOnFill, Search } from 'akar-icons';
|
|
4
4
|
import styles from './styles/Autocomplete.module.scss';
|
|
5
5
|
import fetchUtil from '../../utils/fetchUtil';
|
|
6
6
|
|
|
@@ -20,12 +20,16 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
20
20
|
onSelect,
|
|
21
21
|
setFormData,
|
|
22
22
|
style,
|
|
23
|
+
placeholder = 'Type an address',
|
|
23
24
|
} = props;
|
|
24
25
|
|
|
25
26
|
const [search, setSearch] = useState('');
|
|
26
27
|
const [results, setResults] = useState([]);
|
|
27
28
|
const [isLoading, setIsLoading] = useState(false);
|
|
28
29
|
const [overwrite, setOverwrite] = useState(false);
|
|
30
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
31
|
+
const inputRef = useRef(null);
|
|
32
|
+
const resultsRef = useRef(null);
|
|
29
33
|
|
|
30
34
|
// Memoize URL building logic to avoid recalculations
|
|
31
35
|
const buildSearchUrl = useCallback(
|
|
@@ -67,11 +71,25 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
67
71
|
fetchUtil
|
|
68
72
|
.get(url)
|
|
69
73
|
.then((response) => {
|
|
70
|
-
|
|
74
|
+
// Ensure we're accessing the features array from the response
|
|
75
|
+
// Check if response.data is a string (sometimes happens with certain APIs)
|
|
76
|
+
let parsedData = response.data;
|
|
77
|
+
if (typeof response.data === 'string') {
|
|
78
|
+
try {
|
|
79
|
+
parsedData = JSON.parse(response.data);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error('Error parsing response data:', e);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const features = parsedData?.features || [];
|
|
86
|
+
|
|
87
|
+
setResults(features);
|
|
71
88
|
setIsLoading(false);
|
|
72
89
|
})
|
|
73
90
|
.catch((error) => {
|
|
74
|
-
console.
|
|
91
|
+
console.error('Error fetching address suggestions:', error);
|
|
92
|
+
setResults([]);
|
|
75
93
|
setIsLoading(false);
|
|
76
94
|
});
|
|
77
95
|
},
|
|
@@ -105,15 +123,48 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
105
123
|
|
|
106
124
|
const handleItemClicked = useCallback(
|
|
107
125
|
(place) => {
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
126
|
+
// Use place_name as the primary display value as it contains the full formatted address
|
|
127
|
+
const displayValue = place.place_name || '';
|
|
128
|
+
|
|
129
|
+
// If place_name is not available, construct from components
|
|
130
|
+
const fallbackValue = (() => {
|
|
131
|
+
const address = place.address || '';
|
|
132
|
+
const text = place.text || '';
|
|
133
|
+
const context = place.context
|
|
134
|
+
? place.context
|
|
135
|
+
.filter((ctx) => ctx.text)
|
|
136
|
+
.map((ctx) => ctx.text)
|
|
137
|
+
.join(', ')
|
|
138
|
+
: '';
|
|
139
|
+
|
|
140
|
+
if (address && text) {
|
|
141
|
+
return context
|
|
142
|
+
? `${address} ${text}, ${context}`
|
|
143
|
+
: `${address} ${text}`;
|
|
144
|
+
} else if (text) {
|
|
145
|
+
return context ? `${text}, ${context}` : text;
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
})();
|
|
149
|
+
|
|
150
|
+
const finalValue = displayValue || fallbackValue;
|
|
111
151
|
|
|
112
|
-
setSearch(
|
|
152
|
+
setSearch(finalValue);
|
|
113
153
|
setResults([]);
|
|
114
|
-
|
|
154
|
+
setIsFocused(false);
|
|
155
|
+
|
|
156
|
+
// Update form data with the selected value
|
|
157
|
+
setFormData((prevState) => ({
|
|
158
|
+
...prevState,
|
|
159
|
+
[field]: finalValue,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// Call the onSelect callback with the place object and field
|
|
163
|
+
if (onSelect) {
|
|
164
|
+
onSelect(place, field);
|
|
165
|
+
}
|
|
115
166
|
},
|
|
116
|
-
[field, onSelect]
|
|
167
|
+
[field, onSelect, setFormData]
|
|
117
168
|
);
|
|
118
169
|
|
|
119
170
|
const handleOverwrite = useCallback(() => {
|
|
@@ -148,54 +199,165 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
148
199
|
[style]
|
|
149
200
|
);
|
|
150
201
|
|
|
202
|
+
// Handle focus and blur events
|
|
203
|
+
const handleFocus = useCallback(() => {
|
|
204
|
+
setIsFocused(true);
|
|
205
|
+
if (!overwrite && search && search.length > 1) {
|
|
206
|
+
setIsLoading(true);
|
|
207
|
+
performSearch(1, search);
|
|
208
|
+
}
|
|
209
|
+
}, [overwrite, search, performSearch]);
|
|
210
|
+
|
|
211
|
+
const handleBlur = useCallback(() => {
|
|
212
|
+
// Delay hiding results to allow for click events on results
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
setIsFocused(false);
|
|
215
|
+
}, 200);
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
// Handle click outside to close results
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const handleClickOutside = (event) => {
|
|
221
|
+
if (
|
|
222
|
+
resultsRef.current &&
|
|
223
|
+
!resultsRef.current.contains(event.target) &&
|
|
224
|
+
inputRef.current &&
|
|
225
|
+
!inputRef.current.contains(event.target)
|
|
226
|
+
) {
|
|
227
|
+
setIsFocused(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
232
|
+
return () => {
|
|
233
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
234
|
+
};
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
151
237
|
return (
|
|
152
238
|
<div className={styles.AutocompletePlace}>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
value={search || ''}
|
|
158
|
-
onChange={handleSearchChange}
|
|
159
|
-
placeholder="Type an address"
|
|
160
|
-
autoComplete="off"
|
|
161
|
-
/>
|
|
162
|
-
|
|
163
|
-
{overwrite ? (
|
|
164
|
-
<ToggleOnFill
|
|
165
|
-
data-tooltip-id="system-tooltip"
|
|
166
|
-
data-tooltip-content="Enable Autocomplete"
|
|
167
|
-
strokeWidth={2}
|
|
168
|
-
size={24}
|
|
169
|
-
onClick={handleOverwrite}
|
|
170
|
-
className={styles.toggleActive}
|
|
239
|
+
<div className={styles['AutocompletePlace-inputWrapper']}>
|
|
240
|
+
<Search
|
|
241
|
+
size={16}
|
|
242
|
+
className={styles['AutocompletePlace-searchIcon']}
|
|
171
243
|
/>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
244
|
+
<input
|
|
245
|
+
ref={inputRef}
|
|
246
|
+
className={styles['AutocompletePlace-input']}
|
|
247
|
+
style={inputStyle}
|
|
248
|
+
type="text"
|
|
249
|
+
value={search || ''}
|
|
250
|
+
onChange={handleSearchChange}
|
|
251
|
+
onFocus={handleFocus}
|
|
252
|
+
onBlur={handleBlur}
|
|
253
|
+
placeholder={placeholder}
|
|
254
|
+
autoComplete="off"
|
|
179
255
|
/>
|
|
180
|
-
)}
|
|
181
256
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
257
|
+
{overwrite ? (
|
|
258
|
+
<ToggleOnFill
|
|
259
|
+
data-tooltip-id="system-tooltip"
|
|
260
|
+
data-tooltip-content="Enable Autocomplete"
|
|
261
|
+
strokeWidth={2}
|
|
262
|
+
size={24}
|
|
263
|
+
onClick={handleOverwrite}
|
|
264
|
+
className={styles.toggleActive}
|
|
265
|
+
/>
|
|
266
|
+
) : (
|
|
267
|
+
<ToggleOffFill
|
|
268
|
+
data-tooltip-id="system-tooltip"
|
|
269
|
+
data-tooltip-content="Disable Autocomplete"
|
|
270
|
+
strokeWidth={2}
|
|
271
|
+
size={24}
|
|
272
|
+
onClick={handleOverwrite}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{isFocused && (search.length > 1 || isLoading) && (
|
|
278
|
+
<ul
|
|
279
|
+
ref={resultsRef}
|
|
280
|
+
className={styles['AutocompletePlace-results']}
|
|
281
|
+
>
|
|
282
|
+
{isLoading ? (
|
|
185
283
|
<li
|
|
186
|
-
|
|
187
|
-
className={styles['AutocompletePlace-items']}
|
|
188
|
-
onClick={() => handleItemClicked(place)}
|
|
284
|
+
className={`${styles['AutocompletePlace-items']} ${styles['AutocompletePlace-loading']}`}
|
|
189
285
|
>
|
|
190
|
-
{place.place_name}
|
|
191
|
-
</li>
|
|
192
|
-
))}
|
|
193
|
-
|
|
194
|
-
{isLoading && (
|
|
195
|
-
<li className={styles['AutocompletePlace-items']}>
|
|
196
286
|
Loading...
|
|
197
287
|
</li>
|
|
198
|
-
)
|
|
288
|
+
) : Array.isArray(results) && results.length > 0 ? (
|
|
289
|
+
results.map((place) => (
|
|
290
|
+
<li
|
|
291
|
+
key={place.id}
|
|
292
|
+
className={styles['AutocompletePlace-items']}
|
|
293
|
+
onClick={() => handleItemClicked(place)}
|
|
294
|
+
>
|
|
295
|
+
<div
|
|
296
|
+
className={
|
|
297
|
+
styles['AutocompletePlace-itemMain']
|
|
298
|
+
}
|
|
299
|
+
>
|
|
300
|
+
{place.address && (
|
|
301
|
+
<span
|
|
302
|
+
className={
|
|
303
|
+
styles[
|
|
304
|
+
'AutocompletePlace-itemAddress'
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
>
|
|
308
|
+
{place.address}
|
|
309
|
+
</span>
|
|
310
|
+
)}
|
|
311
|
+
<span
|
|
312
|
+
className={
|
|
313
|
+
styles['AutocompletePlace-itemText']
|
|
314
|
+
}
|
|
315
|
+
>
|
|
316
|
+
{place.text}
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
<div
|
|
320
|
+
className={
|
|
321
|
+
styles[
|
|
322
|
+
'AutocompletePlace-itemSecondary'
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
>
|
|
326
|
+
{place.context &&
|
|
327
|
+
place.context
|
|
328
|
+
.filter((ctx) => ctx.text)
|
|
329
|
+
.map((ctx, index) => (
|
|
330
|
+
<span key={ctx.id || index}>
|
|
331
|
+
{ctx.text}
|
|
332
|
+
</span>
|
|
333
|
+
))
|
|
334
|
+
.reduce(
|
|
335
|
+
(prev, curr, i) => [
|
|
336
|
+
prev,
|
|
337
|
+
<span
|
|
338
|
+
key={`sep-${i}`}
|
|
339
|
+
className={
|
|
340
|
+
styles[
|
|
341
|
+
'AutocompletePlace-itemSeparator'
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
>
|
|
345
|
+
,{' '}
|
|
346
|
+
</span>,
|
|
347
|
+
curr,
|
|
348
|
+
],
|
|
349
|
+
[]
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
</li>
|
|
353
|
+
))
|
|
354
|
+
) : search.length > 1 ? (
|
|
355
|
+
<li
|
|
356
|
+
className={`${styles['AutocompletePlace-items']} ${styles['AutocompletePlace-noResults']}`}
|
|
357
|
+
>
|
|
358
|
+
No results found
|
|
359
|
+
</li>
|
|
360
|
+
) : null}
|
|
199
361
|
</ul>
|
|
200
362
|
)}
|
|
201
363
|
</div>
|
|
@@ -1033,8 +1033,35 @@ function Field({
|
|
|
1033
1033
|
return (
|
|
1034
1034
|
<div className={styles.formbreak}>
|
|
1035
1035
|
<span>
|
|
1036
|
+
<ChevronRight
|
|
1037
|
+
strokeWidth={2}
|
|
1038
|
+
size={16}
|
|
1039
|
+
style={{
|
|
1040
|
+
display: 'inline-block',
|
|
1041
|
+
marginRight: '8px',
|
|
1042
|
+
verticalAlign: 'middle',
|
|
1043
|
+
position: 'relative',
|
|
1044
|
+
top: '-1px',
|
|
1045
|
+
color: 'var(--secondary-color, #fff)',
|
|
1046
|
+
opacity: 0.9,
|
|
1047
|
+
filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.1))',
|
|
1048
|
+
}}
|
|
1049
|
+
/>
|
|
1036
1050
|
{settings.label}{' '}
|
|
1037
|
-
{settings.required === true ?
|
|
1051
|
+
{settings.required === true ? (
|
|
1052
|
+
<span
|
|
1053
|
+
style={{
|
|
1054
|
+
color: 'var(--error-color, #ff6b6b)',
|
|
1055
|
+
textShadow: '0 1px 1px rgba(0,0,0,0.2)',
|
|
1056
|
+
marginLeft: '2px',
|
|
1057
|
+
fontWeight: 'bold',
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
*
|
|
1061
|
+
</span>
|
|
1062
|
+
) : (
|
|
1063
|
+
''
|
|
1064
|
+
)}
|
|
1038
1065
|
</span>
|
|
1039
1066
|
</div>
|
|
1040
1067
|
);
|
|
@@ -459,16 +459,29 @@ function Form({
|
|
|
459
459
|
? [context[3]?.text, context[4]?.text]
|
|
460
460
|
: [context[2]?.text, context[3]?.text];
|
|
461
461
|
|
|
462
|
+
// Determine the prefix based on the id parameter
|
|
463
|
+
let prefix = '';
|
|
464
|
+
if (id !== 'address1') {
|
|
465
|
+
// Extract prefix from id (e.g., 'delivery_address1' -> 'delivery_')
|
|
466
|
+
const match = id.match(/^(.+?)address1$/);
|
|
467
|
+
if (match && match[1]) {
|
|
468
|
+
prefix = match[1];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Create the formDataUpdate object with the appropriate field names
|
|
462
473
|
const formDataUpdate = {
|
|
463
|
-
address1,
|
|
464
|
-
address2,
|
|
465
|
-
suburb,
|
|
466
|
-
postcode,
|
|
467
|
-
state,
|
|
468
|
-
country,
|
|
474
|
+
[`${prefix}address1`]: address1,
|
|
475
|
+
[`${prefix}address2`]: address2,
|
|
476
|
+
[`${prefix}suburb`]: suburb,
|
|
477
|
+
[`${prefix}postcode`]: postcode,
|
|
478
|
+
[`${prefix}state`]: state,
|
|
479
|
+
[`${prefix}country`]: country,
|
|
469
480
|
};
|
|
470
481
|
|
|
471
|
-
|
|
482
|
+
// If same_address is true and we're updating the primary address,
|
|
483
|
+
// also update the postal address fields
|
|
484
|
+
if (formData.same_address === true && id === 'address1') {
|
|
472
485
|
Object.assign(formDataUpdate, {
|
|
473
486
|
postal_address1: address1,
|
|
474
487
|
postal_address2: address2,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronRight } from 'akar-icons';
|
|
3
|
+
import styles from './styles/Field.module.scss';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SectionHeader component for form sections
|
|
7
|
+
* @param {Object} props - Component props
|
|
8
|
+
* @param {string} props.label - The section header text
|
|
9
|
+
* @param {React.ReactNode} [props.icon] - Optional custom icon (defaults to ChevronRight)
|
|
10
|
+
* @returns {React.ReactElement} The section header component
|
|
11
|
+
*/
|
|
12
|
+
const SectionHeader = ({ label, icon }) => {
|
|
13
|
+
return (
|
|
14
|
+
<div className={styles.sectionHeader}>
|
|
15
|
+
{icon || (
|
|
16
|
+
<ChevronRight
|
|
17
|
+
strokeWidth={2}
|
|
18
|
+
size={20}
|
|
19
|
+
style={{
|
|
20
|
+
filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.2))',
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
)}
|
|
24
|
+
<span>{label}</span>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default SectionHeader;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Form Styling Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to use the improved styling for form sections and line breaks.
|
|
4
|
+
|
|
5
|
+
## Section Headers
|
|
6
|
+
|
|
7
|
+
The `SectionHeader` component provides a visually appealing header for form sections. It uses the project's color scheme with fallbacks to ensure good contrast.
|
|
8
|
+
|
|
9
|
+
### Usage
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
import SectionHeader from '../SectionHeader';
|
|
13
|
+
|
|
14
|
+
// In your form component
|
|
15
|
+
<SectionHeader label="Site Details" />
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Props
|
|
19
|
+
|
|
20
|
+
- `label` (string, required): The text to display in the header
|
|
21
|
+
- `icon` (React element, optional): Custom icon to replace the default ChevronRight icon
|
|
22
|
+
|
|
23
|
+
## Line Breaks
|
|
24
|
+
|
|
25
|
+
The `line-break` field type has been enhanced to provide better visual separation between form sections.
|
|
26
|
+
|
|
27
|
+
### Usage
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
<Field
|
|
31
|
+
settings={{
|
|
32
|
+
id: 'delivery_address_section',
|
|
33
|
+
type: 'line-break',
|
|
34
|
+
label: 'Delivery Address #1',
|
|
35
|
+
size: 'full'
|
|
36
|
+
}}
|
|
37
|
+
inputValue=""
|
|
38
|
+
inputClass={inputClass}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Color Scheme Compatibility
|
|
43
|
+
|
|
44
|
+
Both components are designed to work with the project's color scheme variables:
|
|
45
|
+
|
|
46
|
+
- `--primary-color`: Used for background
|
|
47
|
+
- `--secondary-color`: Used for text and accents
|
|
48
|
+
- `--error-color`: Used for required field indicators (*)
|
|
49
|
+
|
|
50
|
+
Fallback colors are provided to ensure good contrast even if the project variables don't provide enough contrast.
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
See `FormWithSectionHeaders.jsx` for a complete example of how to implement a form with the new styling.
|
|
55
|
+
|
|
56
|
+
## Tips for Best Results
|
|
57
|
+
|
|
58
|
+
1. Use `SectionHeader` for main form sections (e.g., "Site Details", "Contact Details")
|
|
59
|
+
2. Use `line-break` for subsections within a main section
|
|
60
|
+
3. Ensure your project's color scheme provides good contrast between primary and secondary colors
|
|
61
|
+
4. If you experience contrast issues, you can override the styles locally:
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
<SectionHeader
|
|
65
|
+
label="Custom Section"
|
|
66
|
+
style={{
|
|
67
|
+
background: '#3a86ff',
|
|
68
|
+
color: '#ffffff'
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
```
|