@visns-studio/visns-components 5.8.12 → 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 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.96.0",
8
- "@nivo/core": "^0.96.0",
9
- "@nivo/line": "^0.96.0",
10
- "@nivo/pie": "^0.96.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.4.1",
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",
@@ -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.12",
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 React, { useState, useEffect, useCallback, useMemo, memo } from 'react';
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
- setResults(response.data.features || []);
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.log(error);
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
- const address = place.address || '';
109
- const text = place.text || '';
110
- const search = address && text ? `${address} ${text}` : text;
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(search);
152
+ setSearch(finalValue);
113
153
  setResults([]);
114
- onSelect(place, field);
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
- <input
154
- className={styles['AutocompletePlace-input']}
155
- style={inputStyle}
156
- type="text"
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
- <ToggleOffFill
174
- data-tooltip-id="system-tooltip"
175
- data-tooltip-content="Disable Autocomplete"
176
- strokeWidth={2}
177
- size={24}
178
- onClick={handleOverwrite}
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
- {results.length > 0 && (
183
- <ul className="AutocompletePlace-results">
184
- {results.map((place) => (
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
- key={place.id}
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
- if (formData.same_address === true || id !== 'address1') {
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
+ ```
@@ -0,0 +1,266 @@
1
+ import React from 'react';
2
+ import SectionHeader from '../SectionHeader';
3
+ import Field from '../Field';
4
+
5
+ /**
6
+ * Example form component that demonstrates how to use the SectionHeader component
7
+ * This shows how to implement the form seen in the screenshot with improved styling
8
+ */
9
+ const FormWithSectionHeaders = ({ formData, setFormData, onChange }) => {
10
+ // Sample form settings for demonstration
11
+ const formSettings = {
12
+ // Form settings would go here
13
+ };
14
+
15
+ // Sample input classes
16
+ const inputClass = {
17
+ // Input classes would go here
18
+ };
19
+
20
+ return (
21
+ <form>
22
+ {/* Site Details Section */}
23
+ <SectionHeader label="Site Details" />
24
+
25
+ {/* Site Name Field */}
26
+ <Field
27
+ settings={{
28
+ id: 'site_name',
29
+ type: 'text',
30
+ label: 'Site Name',
31
+ required: true,
32
+ size: 'half'
33
+ }}
34
+ inputValue={formData.site_name}
35
+ inputClass={inputClass}
36
+ onChange={onChange}
37
+ />
38
+
39
+ {/* Address Fields */}
40
+ <Field
41
+ settings={{
42
+ id: 'address_1',
43
+ type: 'text',
44
+ label: 'Address #1',
45
+ required: true,
46
+ size: 'half'
47
+ }}
48
+ inputValue={formData.address_1}
49
+ inputClass={inputClass}
50
+ onChange={onChange}
51
+ />
52
+
53
+ <Field
54
+ settings={{
55
+ id: 'address_2',
56
+ type: 'text',
57
+ label: 'Address #2',
58
+ size: 'half'
59
+ }}
60
+ inputValue={formData.address_2}
61
+ inputClass={inputClass}
62
+ onChange={onChange}
63
+ />
64
+
65
+ {/* Suburb Field */}
66
+ <Field
67
+ settings={{
68
+ id: 'suburb',
69
+ type: 'text',
70
+ label: 'Suburb',
71
+ required: true,
72
+ size: 'half'
73
+ }}
74
+ inputValue={formData.suburb}
75
+ inputClass={inputClass}
76
+ onChange={onChange}
77
+ />
78
+
79
+ {/* Postcode Field */}
80
+ <Field
81
+ settings={{
82
+ id: 'postcode',
83
+ type: 'text',
84
+ label: 'Postcode',
85
+ required: true,
86
+ size: 'half'
87
+ }}
88
+ inputValue={formData.postcode}
89
+ inputClass={inputClass}
90
+ onChange={onChange}
91
+ />
92
+
93
+ {/* State Field */}
94
+ <Field
95
+ settings={{
96
+ id: 'state',
97
+ type: 'text',
98
+ label: 'State',
99
+ required: true,
100
+ size: 'half'
101
+ }}
102
+ inputValue={formData.state}
103
+ inputClass={inputClass}
104
+ onChange={onChange}
105
+ />
106
+
107
+ {/* Country Field */}
108
+ <Field
109
+ settings={{
110
+ id: 'country',
111
+ type: 'text',
112
+ label: 'Country',
113
+ required: true,
114
+ size: 'half'
115
+ }}
116
+ inputValue={formData.country}
117
+ inputClass={inputClass}
118
+ onChange={onChange}
119
+ />
120
+
121
+ {/* Delivery Address Section */}
122
+ <Field
123
+ settings={{
124
+ id: 'delivery_address_section',
125
+ type: 'line-break',
126
+ label: 'Delivery Address #1',
127
+ size: 'full'
128
+ }}
129
+ inputValue=""
130
+ inputClass={inputClass}
131
+ />
132
+
133
+ {/* Delivery Address Fields */}
134
+ <Field
135
+ settings={{
136
+ id: 'delivery_address_1',
137
+ type: 'text',
138
+ label: 'Delivery Address #1',
139
+ required: true,
140
+ size: 'half'
141
+ }}
142
+ inputValue={formData.delivery_address_1}
143
+ inputClass={inputClass}
144
+ onChange={onChange}
145
+ />
146
+
147
+ <Field
148
+ settings={{
149
+ id: 'delivery_address_2',
150
+ type: 'text',
151
+ label: 'Delivery Address #2',
152
+ size: 'half'
153
+ }}
154
+ inputValue={formData.delivery_address_2}
155
+ inputClass={inputClass}
156
+ onChange={onChange}
157
+ />
158
+
159
+ {/* Delivery Suburb Field */}
160
+ <Field
161
+ settings={{
162
+ id: 'delivery_suburb',
163
+ type: 'text',
164
+ label: 'Delivery Suburb',
165
+ required: true,
166
+ size: 'half'
167
+ }}
168
+ inputValue={formData.delivery_suburb}
169
+ inputClass={inputClass}
170
+ onChange={onChange}
171
+ />
172
+
173
+ {/* Delivery Postcode Field */}
174
+ <Field
175
+ settings={{
176
+ id: 'delivery_postcode',
177
+ type: 'text',
178
+ label: 'Delivery Postcode',
179
+ required: true,
180
+ size: 'half'
181
+ }}
182
+ inputValue={formData.delivery_postcode}
183
+ inputClass={inputClass}
184
+ onChange={onChange}
185
+ />
186
+
187
+ {/* Delivery State Field */}
188
+ <Field
189
+ settings={{
190
+ id: 'delivery_state',
191
+ type: 'text',
192
+ label: 'Delivery State',
193
+ required: true,
194
+ size: 'half'
195
+ }}
196
+ inputValue={formData.delivery_state}
197
+ inputClass={inputClass}
198
+ onChange={onChange}
199
+ />
200
+
201
+ {/* Delivery Country Field */}
202
+ <Field
203
+ settings={{
204
+ id: 'delivery_country',
205
+ type: 'text',
206
+ label: 'Delivery Country',
207
+ required: true,
208
+ size: 'half'
209
+ }}
210
+ inputValue={formData.delivery_country}
211
+ inputClass={inputClass}
212
+ onChange={onChange}
213
+ />
214
+
215
+ {/* Contact Details Section */}
216
+ <SectionHeader label="Contact Details" />
217
+
218
+ {/* Firstname Field */}
219
+ <Field
220
+ settings={{
221
+ id: 'firstname',
222
+ type: 'text',
223
+ label: 'Firstname',
224
+ required: true,
225
+ size: 'half'
226
+ }}
227
+ inputValue={formData.firstname}
228
+ inputClass={inputClass}
229
+ onChange={onChange}
230
+ />
231
+
232
+ {/* Surname Field */}
233
+ <Field
234
+ settings={{
235
+ id: 'surname',
236
+ type: 'text',
237
+ label: 'Surname',
238
+ size: 'half'
239
+ }}
240
+ inputValue={formData.surname}
241
+ inputClass={inputClass}
242
+ onChange={onChange}
243
+ />
244
+
245
+ {/* Save Button */}
246
+ <div style={{ width: '100%', textAlign: 'center', marginTop: '20px' }}>
247
+ <button
248
+ type="submit"
249
+ style={{
250
+ background: 'var(--primary-color, #2684FF)',
251
+ color: 'var(--secondary-color, #fff)',
252
+ padding: '10px 20px',
253
+ border: 'none',
254
+ borderRadius: 'var(--br, 3px)',
255
+ cursor: 'pointer',
256
+ fontWeight: 'bold'
257
+ }}
258
+ >
259
+ Save & Close
260
+ </button>
261
+ </div>
262
+ </form>
263
+ );
264
+ };
265
+
266
+ export default FormWithSectionHeaders;
@@ -1496,6 +1496,8 @@ function GenericIndex({
1496
1496
 
1497
1497
  // Add a ref to store the latest rowsSelected value
1498
1498
  const rowsSelectedRef = useRef({});
1499
+ // Create a ref to track if we've already processed active filter combinations
1500
+ const activeFilterRef = useRef(null);
1499
1501
 
1500
1502
  // Update the ref whenever rowsSelected changes
1501
1503
  useEffect(() => {
@@ -1847,9 +1849,6 @@ function GenericIndex({
1847
1849
  return;
1848
1850
  }
1849
1851
 
1850
- // Create a ref to track if we've already processed this combination
1851
- const activeFilterRef = useRef(null);
1852
-
1853
1852
  // Find the active filter and its child
1854
1853
  const activeFilter = subnav?.find((filter) => {
1855
1854
  if (filter.isParent) {
@@ -1,36 +1,120 @@
1
- .AutocompletePlace-results {
2
- z-index: 999;
1
+ .AutocompletePlace {
2
+ position: relative;
3
+ width: 100%;
4
+
5
+ .toggleActive {
6
+ color: var(--secondary-color);
7
+ }
3
8
  }
4
9
 
5
- .AutocompletePlace {
10
+ .AutocompletePlace-inputWrapper {
6
11
  position: relative;
12
+ display: flex;
13
+ align-items: center;
7
14
  width: 100%;
8
15
 
9
- > svg {
16
+ > svg:not(.AutocompletePlace-searchIcon) {
10
17
  position: absolute;
11
- right: 0.5em;
12
- top: 0.95rem;
13
- color: var(--paragraph-color);
18
+ right: 0.5rem;
19
+ top: 50%;
20
+ transform: translateY(-50%);
14
21
  cursor: pointer;
15
22
  }
23
+ }
16
24
 
17
- .toggleActive {
18
- color: var(--secondary-color);
19
- }
25
+ .AutocompletePlace-searchIcon {
26
+ position: absolute;
27
+ left: 0.75rem;
28
+ color: var(--paragraph-color);
29
+ pointer-events: none;
20
30
  }
21
31
 
22
32
  .AutocompletePlace-input {
23
- padding: 0.65rem 3rem 0.65rem 0.95rem !important;
33
+ padding: 0.65rem 3rem 0.65rem 2.25rem !important;
24
34
  height: 38px;
35
+ width: 100%;
36
+ border: 1px solid rgba(var(--primary-rgb), 0.2);
37
+ border-radius: 4px;
38
+ outline: none;
39
+ font-size: 0.9rem;
40
+
41
+ &:focus {
42
+ border-color: rgba(var(--primary-rgb), 0.5);
43
+ box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1);
44
+ }
45
+ }
46
+
47
+ .AutocompletePlace-results {
48
+ position: absolute;
49
+ top: 100%;
50
+ left: 0;
51
+ right: 0;
52
+ z-index: 999;
53
+ margin: 0;
54
+ padding: 0;
55
+ background-color: #fff;
56
+ border: 1px solid rgba(var(--primary-rgb), 0.2);
57
+ border-radius: 4px;
58
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
59
+ max-height: 250px;
60
+ overflow-y: auto;
61
+ margin-top: 4px;
62
+ list-style-type: none;
63
+ width: 100%;
25
64
  }
26
65
 
27
66
  .AutocompletePlace-items {
28
67
  list-style: none;
29
68
  border-bottom: 1px solid rgba(var(--primary-rgb), 0.1);
30
69
  cursor: pointer;
31
- padding: 5px;
70
+ padding: 8px 12px;
71
+ font-size: 0.9rem;
72
+
73
+ &:last-child {
74
+ border-bottom: none;
75
+ }
32
76
 
33
77
  &:hover {
34
- background-color: rgba(var(--paragraph-rgb), 0.065);
78
+ background-color: rgba(var(--primary-rgb), 0.05);
35
79
  }
36
80
  }
81
+
82
+ .AutocompletePlace-itemMain {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 4px;
86
+ margin-bottom: 2px;
87
+ }
88
+
89
+ .AutocompletePlace-itemAddress {
90
+ font-weight: 600;
91
+ color: var(--primary-color);
92
+ }
93
+
94
+ .AutocompletePlace-itemText {
95
+ color: var(--paragraph-color);
96
+ }
97
+
98
+ .AutocompletePlace-itemSecondary {
99
+ font-size: 0.8rem;
100
+ color: rgba(var(--paragraph-rgb), 0.7);
101
+ white-space: nowrap;
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ }
105
+
106
+ .AutocompletePlace-itemSeparator {
107
+ opacity: 0.6;
108
+ }
109
+
110
+ .AutocompletePlace-loading {
111
+ font-style: italic;
112
+ color: var(--paragraph-color);
113
+ text-align: center;
114
+ }
115
+
116
+ .AutocompletePlace-noResults {
117
+ font-style: italic;
118
+ color: var(--paragraph-color);
119
+ text-align: center;
120
+ }
@@ -494,18 +494,101 @@ input[type='file']:hover {
494
494
  margin-right: 5px;
495
495
  }
496
496
 
497
+ /* Section header styling for form sections */
498
+ .sectionHeader {
499
+ display: flex;
500
+ align-items: center;
501
+ width: 100%;
502
+ padding: 0.85em 1.2em;
503
+ background: var(--primary-color, #2684ff);
504
+ border-radius: var(--br, 3px);
505
+ box-sizing: border-box;
506
+ margin: 0.5em 0 1em 0;
507
+ position: relative;
508
+ overflow: hidden;
509
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
510
+
511
+ /* Add a subtle gradient overlay for depth */
512
+ &::before {
513
+ content: '';
514
+ position: absolute;
515
+ top: 0;
516
+ left: 0;
517
+ width: 100%;
518
+ height: 100%;
519
+ background: linear-gradient(
520
+ to right,
521
+ rgba(255, 255, 255, 0.15),
522
+ rgba(255, 255, 255, 0.05)
523
+ );
524
+ pointer-events: none;
525
+ }
526
+
527
+ svg {
528
+ margin-right: 8px;
529
+ color: var(--secondary-color, #fff);
530
+ }
531
+
532
+ span {
533
+ font-size: 1em;
534
+ color: var(--secondary-color, #fff);
535
+ font-weight: 600;
536
+ letter-spacing: 0.02em;
537
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
538
+ }
539
+ }
540
+
541
+ /* Line break styling for form sections */
497
542
  .formbreak {
498
543
  display: block;
499
544
  width: 100%;
500
- padding: 1em;
501
- background: rgba(var(--primary-rgb), 0.95);
502
- border-radius: 3px;
545
+ padding: 0.85em 1.2em;
546
+ /* Use a more reliable background that works with any color scheme */
547
+ background: var(--primary-color, #2684ff);
548
+ border-radius: var(--br, 3px);
503
549
  box-sizing: border-box;
504
- border: 2px solid rgba(var(--primary-rgb), 1.1);
550
+ border-left: 4px solid var(--secondary-color, #fff);
551
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
552
+ margin: 0.5em 0;
553
+ position: relative;
554
+ overflow: hidden;
555
+
556
+ /* Add a subtle gradient overlay for depth */
557
+ &::before {
558
+ content: '';
559
+ position: absolute;
560
+ top: 0;
561
+ left: 0;
562
+ width: 100%;
563
+ height: 100%;
564
+ background: linear-gradient(
565
+ to right,
566
+ rgba(255, 255, 255, 0.1),
567
+ rgba(255, 255, 255, 0)
568
+ );
569
+ pointer-events: none;
570
+ }
571
+
572
+ &::after {
573
+ content: '';
574
+ position: absolute;
575
+ bottom: 0;
576
+ left: 0;
577
+ width: 100%;
578
+ height: 2px;
579
+ background: var(--tertiary-color, #fff);
580
+ opacity: 0.3;
581
+ }
505
582
 
506
583
  span {
507
584
  font-size: 0.95em;
508
- color: var(--secondary-color);
585
+ color: var(--tertiary-color, #fff);
586
+ font-weight: 500;
587
+ letter-spacing: 0.01em;
588
+ display: flex;
589
+ align-items: center;
590
+ position: relative;
591
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
509
592
  }
510
593
  }
511
594