@visns-studio/visns-components 5.5.2 → 5.5.3

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
@@ -9,7 +9,7 @@
9
9
  "@nivo/line": "^0.88.0",
10
10
  "@nivo/pie": "^0.88.0",
11
11
  "@tinymce/tinymce-react": "^6.1.0",
12
- "@vitejs/plugin-react": "^4.3.4",
12
+ "@vitejs/plugin-react": "^4.4.0",
13
13
  "add": "^2.0.6",
14
14
  "akar-icons": "^1.9.31",
15
15
  "array-move": "^4.0.0",
@@ -17,7 +17,7 @@
17
17
  "axios": "^1.8.4",
18
18
  "browser-image-compression": "^2.0.2",
19
19
  "dayjs": "^1.11.13",
20
- "fabric": "^6.6.2",
20
+ "fabric": "^6.6.4",
21
21
  "file-saver": "^2.0.5",
22
22
  "framer-motion": "^11.18.2",
23
23
  "html-react-parser": "^5.2.3",
@@ -54,12 +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.18.0",
57
+ "sweetalert2": "^11.19.1",
58
58
  "truncate": "^3.0.0",
59
59
  "uuid": "^10.0.0",
60
60
  "validator": "^13.15.0",
61
61
  "vite": "^5.4.18",
62
- "yarn": "^1.22.22"
62
+ "yarn": "^1.22.22",
63
+ "yet-another-react-lightbox": "^3.22.0"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@babel/core": "^7.26.10",
@@ -81,7 +82,7 @@
81
82
  "react-dom": "^17.0.0 || ^18.0.0"
82
83
  },
83
84
  "name": "@visns-studio/visns-components",
84
- "version": "5.5.2",
85
+ "version": "5.5.3",
85
86
  "description": "Various packages to assist in the development of our Custom Applications.",
86
87
  "main": "src/index.js",
87
88
  "files": [
@@ -16,6 +16,7 @@ import numeral from 'numeral';
16
16
  import Popup from 'reactjs-popup';
17
17
  import 'reactjs-popup/dist/index.css';
18
18
  import parse from 'html-react-parser';
19
+ import 'react-toggle/style.css';
19
20
  import { saveAs } from 'file-saver';
20
21
  import NumberFilter from '@inovua/reactdatagrid-community/NumberFilter';
21
22
  import SelectFilter from '@inovua/reactdatagrid-community/SelectFilter';
@@ -25,9 +26,12 @@ import ReactDataGrid from '@inovua/reactdatagrid-community';
25
26
  // import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
26
27
  // import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
27
28
  // import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
28
- import { Gallery } from 'react-grid-gallery';
29
+ // import { Gallery } from 'react-grid-gallery';
29
30
  import { confirmAlert } from 'react-confirm-alert';
30
31
  import { toast } from 'react-toastify';
32
+ import imageCompression from 'browser-image-compression';
33
+ import Vapor from 'laravel-vapor';
34
+ import Lightbox from 'yet-another-react-lightbox';
31
35
  import {
32
36
  Alarm,
33
37
  ArrowCounterClockwise,
@@ -59,6 +63,7 @@ import styles from './styles/DataGrid.module.scss';
59
63
 
60
64
  import '@inovua/reactdatagrid-community/index.css';
61
65
  import 'react-confirm-alert/src/react-confirm-alert.css';
66
+ import 'yet-another-react-lightbox/styles.css';
62
67
 
63
68
  import CustomFetch from './Fetch';
64
69
  import Download from './Download';
@@ -166,6 +171,13 @@ const DataGrid = forwardRef(
166
171
  /** Gallery Modal States */
167
172
  const [modalGalleryShow, setModalGalleryShow] = useState(false);
168
173
  const [galleryData, setGalleryData] = useState([]);
174
+ const [uploadProgress, setUploadProgress] = useState(0);
175
+ const [currentItem, setCurrentItem] = useState(null);
176
+ const [isUploading, setIsUploading] = useState(false);
177
+
178
+ /** Lightbox States */
179
+ const [lightboxOpen, setLightboxOpen] = useState(false);
180
+ const [lightboxIndex, setLightboxIndex] = useState(0);
169
181
 
170
182
  /** Gallery Modal Functions */
171
183
  const modalGalleryOpen = () => {
@@ -176,6 +188,149 @@ const DataGrid = forwardRef(
176
188
  setModalGalleryShow(false);
177
189
  };
178
190
 
191
+ const handleFileUpload = async (files) => {
192
+ if (!currentItem || !currentItem.settings.upload) return;
193
+
194
+ setIsUploading(true);
195
+ const uploadPromises = [];
196
+
197
+ for (let i = 0; i < files.length; i++) {
198
+ const file = files[i];
199
+
200
+ try {
201
+ // Optimize image if needed
202
+ let optimizedFile = file;
203
+ if (file.type.startsWith('image/')) {
204
+ const options = {
205
+ maxSizeMB: 1,
206
+ maxWidthOrHeight: 1920,
207
+ useWebWorker: true,
208
+ };
209
+ try {
210
+ optimizedFile = await imageCompression(
211
+ file,
212
+ options
213
+ );
214
+ } catch (err) {
215
+ console.error('Image compression failed:', err);
216
+ // Continue with original file if compression fails
217
+ }
218
+ }
219
+
220
+ // Upload to Vapor
221
+ const uploadPromise = new Promise((resolve, reject) => {
222
+ Vapor.store(optimizedFile, {
223
+ progress: (progress) => {
224
+ setUploadProgress(progress * 100);
225
+ },
226
+ })
227
+ .then((response) => {
228
+ // Construct the URL with the key parameter
229
+ const uploadSettings =
230
+ currentItem.settings.upload;
231
+ const itemData = currentItem.data;
232
+ const keyValue = itemData[uploadSettings.key];
233
+ const url = keyValue
234
+ ? `${uploadSettings.url}/${keyValue}`
235
+ : uploadSettings.url;
236
+
237
+ // Make the PUT request
238
+ CustomFetch(
239
+ url,
240
+ uploadSettings.method || 'POST',
241
+ {
242
+ uuid: response.uuid,
243
+ key: response.key,
244
+ bucket: response.bucket,
245
+ filename: file.name,
246
+ filesize: optimizedFile.size,
247
+ extension: response.extension,
248
+ file_relationship: uploadSettings.id,
249
+ },
250
+ (result) => {
251
+ if (result.error === '') {
252
+ resolve();
253
+ } else {
254
+ toast.error(String(result.error));
255
+ reject(result.error);
256
+ }
257
+ }
258
+ );
259
+ })
260
+ .catch(reject);
261
+ });
262
+
263
+ uploadPromises.push(uploadPromise);
264
+ } catch (error) {
265
+ console.error('Error uploading file:', error);
266
+ toast.error(
267
+ `Error uploading ${file.name}: ${
268
+ error.message || 'Unknown error'
269
+ }`
270
+ );
271
+ }
272
+ }
273
+
274
+ try {
275
+ await Promise.all(uploadPromises);
276
+ toast.success('Files uploaded successfully');
277
+
278
+ // Refresh the gallery data
279
+ if (currentItem && currentItem.settings && currentItem.data) {
280
+ const s = currentItem.settings;
281
+ const d = currentItem.data;
282
+
283
+ // Function to access nested property using a series of keys
284
+ const getNestedValue = (data, keys) =>
285
+ keys.reduce(
286
+ (currentValue, key) =>
287
+ currentValue && currentValue[key] !== undefined
288
+ ? currentValue[key]
289
+ : undefined,
290
+ data
291
+ );
292
+
293
+ // Fetch updated data
294
+ try {
295
+ const res = await CustomFetch(
296
+ `${form.url}/${d[form.primaryKey]}`,
297
+ 'GET',
298
+ {}
299
+ );
300
+
301
+ if (res.data) {
302
+ // Extract the updated nested value
303
+ const updatedNestedValue = getNestedValue(
304
+ res.data,
305
+ s.relation
306
+ );
307
+
308
+ // Update gallery data
309
+ if (
310
+ updatedNestedValue &&
311
+ Array.isArray(updatedNestedValue)
312
+ ) {
313
+ const updatedGalleryArray =
314
+ updatedNestedValue.map((item) => ({
315
+ src: item[s.key],
316
+ caption: item.file_name,
317
+ }));
318
+
319
+ setGalleryData(updatedGalleryArray);
320
+ }
321
+ }
322
+ } catch (error) {
323
+ console.error('Error refreshing gallery data:', error);
324
+ }
325
+ }
326
+ } catch (error) {
327
+ console.error('Error in upload process:', error);
328
+ } finally {
329
+ setIsUploading(false);
330
+ setUploadProgress(0);
331
+ }
332
+ };
333
+
179
334
  /** Modal States */
180
335
  const [formData, setFormData] = useState({});
181
336
  const [formType, setFormType] = useState('');
@@ -601,13 +756,16 @@ const DataGrid = forwardRef(
601
756
  // Extracting nested value using predefined keys from 's.relation'
602
757
  const nestedValue = getNestedValue(d, s.relation);
603
758
 
604
- // Mapping nested values to create gallery array with source and captions
759
+ // Mapping nested values to create gallery array with source, captions, and dimensions
605
760
  const galleryArray = nestedValue.map((item) => ({
606
761
  src: item[s.key],
607
762
  caption: item.file_name,
763
+ width: 320, // Default width for proper grid layout
764
+ height: 240, // Default height for proper grid layout
608
765
  }));
609
766
 
610
767
  setGalleryData(galleryArray);
768
+ setCurrentItem({ data: d, settings: s });
611
769
 
612
770
  modalGalleryOpen();
613
771
  break;
@@ -2461,11 +2619,9 @@ const DataGrid = forwardRef(
2461
2619
  if (shouldRenderButton) {
2462
2620
  return (
2463
2621
  <button
2464
- className={styles.btn}
2465
- style={{
2466
- padding:
2467
- '0.2em 1em 0.05em 1em',
2468
- }}
2622
+ className={
2623
+ styles.clockButton
2624
+ }
2469
2625
  onClick={(e) => {
2470
2626
  e.stopPropagation();
2471
2627
  e.preventDefault();
@@ -2497,9 +2653,9 @@ const DataGrid = forwardRef(
2497
2653
  data-tooltip-content={`${column.label} Timer`}
2498
2654
  strokeWidth={2}
2499
2655
  size={18}
2500
- className={
2501
- styles.tdaction
2502
- }
2656
+ style={{
2657
+ color: 'white',
2658
+ }}
2503
2659
  />
2504
2660
  </button>
2505
2661
  );
@@ -2983,6 +3139,8 @@ const DataGrid = forwardRef(
2983
3139
  open={modalGalleryShow}
2984
3140
  onClose={modalGalleryClose}
2985
3141
  closeOnDocumentClick={false}
3142
+ contentStyle={{ width: 'auto', maxWidth: '90vw' }}
3143
+ overlayStyle={{ background: 'rgba(0, 0, 0, 0.7)' }}
2986
3144
  >
2987
3145
  <div
2988
3146
  className={`${styles.modalwrap} ${styles['top--modal']}`}
@@ -2997,15 +3155,191 @@ const DataGrid = forwardRef(
2997
3155
  <CircleX strokeWidth={1} size={24} />
2998
3156
  </button>
2999
3157
  </div>
3000
- <div className={styles.modal__content}>
3158
+ <div
3159
+ className={`${styles.modal__content} ${styles['modal__content--gallery']}`}
3160
+ >
3161
+ {currentItem?.settings?.upload && (
3162
+ <div className={styles.uploadSection}>
3163
+ <h3>
3164
+ <Image
3165
+ strokeWidth={1.5}
3166
+ size={18}
3167
+ />
3168
+ Upload New Images
3169
+ </h3>
3170
+ <div
3171
+ className={styles.dropzoneContainer}
3172
+ >
3173
+ {isUploading && (
3174
+ <div
3175
+ className={
3176
+ styles.progressBar
3177
+ }
3178
+ >
3179
+ <div
3180
+ className={
3181
+ styles.progressBarInner
3182
+ }
3183
+ style={{
3184
+ width: `${uploadProgress}%`,
3185
+ }}
3186
+ ></div>
3187
+ </div>
3188
+ )}
3189
+ <div
3190
+ className={styles.dropzone}
3191
+ onClick={() =>
3192
+ document
3193
+ .getElementById(
3194
+ 'gallery-file-upload'
3195
+ )
3196
+ .click()
3197
+ }
3198
+ onDragOver={(e) => {
3199
+ e.preventDefault();
3200
+ e.stopPropagation();
3201
+ }}
3202
+ onDragEnter={(e) => {
3203
+ e.preventDefault();
3204
+ e.stopPropagation();
3205
+ }}
3206
+ onDragLeave={(e) => {
3207
+ e.preventDefault();
3208
+ e.stopPropagation();
3209
+ }}
3210
+ onDrop={(e) => {
3211
+ e.preventDefault();
3212
+ e.stopPropagation();
3213
+ const files =
3214
+ e.dataTransfer.files;
3215
+ if (files.length > 0) {
3216
+ handleFileUpload(files);
3217
+ }
3218
+ }}
3219
+ >
3220
+ <CloudUpload
3221
+ strokeWidth={1.5}
3222
+ size={24}
3223
+ />
3224
+ <p>
3225
+ Drag and drop files here or
3226
+ click to select files
3227
+ </p>
3228
+ <input
3229
+ id="gallery-file-upload"
3230
+ type="file"
3231
+ multiple
3232
+ style={{ display: 'none' }}
3233
+ onChange={(e) => {
3234
+ if (
3235
+ e.target.files
3236
+ .length > 0
3237
+ ) {
3238
+ handleFileUpload(
3239
+ e.target.files
3240
+ );
3241
+ e.target.value =
3242
+ null; // Reset input
3243
+ }
3244
+ }}
3245
+ accept="image/*"
3246
+ />
3247
+ </div>
3248
+ </div>
3249
+ </div>
3250
+ )}
3001
3251
  <div className={styles.formcontainer}>
3002
3252
  {galleryData.length > 0 ? (
3003
- <Gallery
3004
- images={galleryData}
3005
- enableImageSelection={false}
3006
- />
3253
+ <div
3254
+ className={styles.galleryContainer}
3255
+ >
3256
+ <div
3257
+ className={
3258
+ styles.galleryInstructions
3259
+ }
3260
+ >
3261
+ <Image
3262
+ strokeWidth={1.5}
3263
+ size={16}
3264
+ />
3265
+ <span>
3266
+ Tap any image to enlarge
3267
+ </span>
3268
+ </div>
3269
+ <div
3270
+ className={
3271
+ styles.customGalleryGrid
3272
+ }
3273
+ >
3274
+ {galleryData.map(
3275
+ (img, index) => (
3276
+ <div
3277
+ key={`gallery-item-${index}`}
3278
+ className={
3279
+ styles.galleryItem
3280
+ }
3281
+ onClick={() => {
3282
+ setLightboxIndex(
3283
+ index
3284
+ );
3285
+ setLightboxOpen(
3286
+ true
3287
+ );
3288
+ }}
3289
+ >
3290
+ <img
3291
+ src={img.src}
3292
+ alt={
3293
+ img.caption ||
3294
+ `Image ${
3295
+ index +
3296
+ 1
3297
+ }`
3298
+ }
3299
+ />
3300
+ {img.caption && (
3301
+ <div
3302
+ className={
3303
+ styles.imageCaption
3304
+ }
3305
+ >
3306
+ {
3307
+ img.caption
3308
+ }
3309
+ </div>
3310
+ )}
3311
+ <div
3312
+ className={
3313
+ styles.galleryImageOverlay
3314
+ }
3315
+ >
3316
+ <span>
3317
+ <Image
3318
+ strokeWidth={
3319
+ 1.5
3320
+ }
3321
+ size={
3322
+ 16
3323
+ }
3324
+ style={{
3325
+ marginRight:
3326
+ '6px',
3327
+ }}
3328
+ />
3329
+ Enlarge
3330
+ </span>
3331
+ </div>
3332
+ </div>
3333
+ )
3334
+ )}
3335
+ </div>
3336
+ </div>
3007
3337
  ) : (
3008
3338
  <div className={styles.noDataMessage}>
3339
+ <Image
3340
+ strokeWidth={1.5}
3341
+ size={20}
3342
+ />
3009
3343
  No images available in the gallery
3010
3344
  </div>
3011
3345
  )}
@@ -3014,6 +3348,14 @@ const DataGrid = forwardRef(
3014
3348
  </div>
3015
3349
  </div>
3016
3350
  </Popup>
3351
+
3352
+ {/* Lightbox for full-size image viewing */}
3353
+ <Lightbox
3354
+ open={lightboxOpen}
3355
+ close={() => setLightboxOpen(false)}
3356
+ index={lightboxIndex}
3357
+ slides={galleryData.map((img) => ({ src: img.src }))}
3358
+ />
3017
3359
  </>
3018
3360
  );
3019
3361
  }
@@ -101,7 +101,7 @@ function MultiSelect({
101
101
 
102
102
  return (
103
103
  <SelectComponent
104
- closeMenuOnSelect={!multi}
104
+ closeMenuOnSelect={!multi || settings.limit === 1}
105
105
  isClearable
106
106
  isSearchable
107
107
  isMulti={multi}
@@ -109,7 +109,19 @@ function MultiSelect({
109
109
  components={{ SelectList }}
110
110
  options={selectOptions}
111
111
  onChange={(inputValue, action) => {
112
- onChange(inputValue, action, settings.id);
112
+ // Special handling for limit=1: keep only the most recent selection
113
+ if (
114
+ settings.limit === 1 &&
115
+ multi &&
116
+ Array.isArray(inputValue) &&
117
+ inputValue.length > 1
118
+ ) {
119
+ const latestSelection = inputValue[inputValue.length - 1];
120
+ const newValue = latestSelection ? [latestSelection] : [];
121
+ onChange(newValue, action, settings.id);
122
+ } else {
123
+ onChange(inputValue, action, settings.id);
124
+ }
113
125
  }}
114
126
  className={
115
127
  className
@@ -130,6 +142,11 @@ function MultiSelect({
130
142
  value={selectValue}
131
143
  onCreateOption={handleCreate}
132
144
  placeholder={placeholder ? placeholder : 'Select...'}
145
+ isOptionDisabled={
146
+ settings.limit && multi && settings.limit > 1
147
+ ? () => selectValue.length >= settings.limit
148
+ : undefined
149
+ }
133
150
  />
134
151
  );
135
152
  }
@@ -21,6 +21,148 @@
21
21
  }
22
22
  }
23
23
 
24
+ /* Clock button styles */
25
+ .clockButton {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ width: 40px;
30
+ height: 40px;
31
+ border-radius: 4px;
32
+ background-color: #2b6cb0; /* Matching blue from screenshot */
33
+ border: none;
34
+ cursor: pointer;
35
+ transition: all 0.2s ease;
36
+ margin: 0 auto;
37
+
38
+ &:hover {
39
+ background-color: #2563a0; /* Slightly darker blue on hover */
40
+ }
41
+
42
+ svg {
43
+ color: white; /* White icon color */
44
+ }
45
+ }
46
+
47
+ /* Toggle button styles */
48
+ :global {
49
+ .react-toggle {
50
+ touch-action: pan-x;
51
+ display: inline-block;
52
+ position: relative;
53
+ cursor: pointer;
54
+ background-color: transparent;
55
+ border: 0;
56
+ padding: 0;
57
+ -webkit-touch-callout: none;
58
+ -webkit-user-select: none;
59
+ -khtml-user-select: none;
60
+ -moz-user-select: none;
61
+ -ms-user-select: none;
62
+ user-select: none;
63
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
64
+ -webkit-tap-highlight-color: transparent;
65
+ }
66
+
67
+ .react-toggle-screenreader-only {
68
+ border: 0;
69
+ clip: rect(0 0 0 0);
70
+ height: 1px;
71
+ margin: -1px;
72
+ overflow: hidden;
73
+ padding: 0;
74
+ position: absolute;
75
+ width: 1px;
76
+ }
77
+
78
+ .react-toggle-track {
79
+ width: 50px;
80
+ height: 24px;
81
+ padding: 0;
82
+ border-radius: 30px;
83
+ background-color: #4d4d4d;
84
+ transition: all 0.2s ease;
85
+ }
86
+
87
+ .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
88
+ background-color: #000000;
89
+ }
90
+
91
+ .react-toggle--checked .react-toggle-track {
92
+ background-color: var(--primary-color, #3b82f6);
93
+ }
94
+
95
+ .react-toggle--checked:hover:not(.react-toggle--disabled)
96
+ .react-toggle-track {
97
+ background-color: var(--primary-color, #3b82f6);
98
+ opacity: 0.8;
99
+ }
100
+
101
+ .react-toggle-track-check {
102
+ position: absolute;
103
+ width: 14px;
104
+ height: 10px;
105
+ top: 0px;
106
+ bottom: 0px;
107
+ margin-top: auto;
108
+ margin-bottom: auto;
109
+ line-height: 0;
110
+ left: 8px;
111
+ opacity: 0;
112
+ transition: opacity 0.25s ease;
113
+ }
114
+
115
+ .react-toggle--checked .react-toggle-track-check {
116
+ opacity: 1;
117
+ transition: opacity 0.25s ease;
118
+ }
119
+
120
+ .react-toggle-track-x {
121
+ position: absolute;
122
+ width: 10px;
123
+ height: 10px;
124
+ top: 0px;
125
+ bottom: 0px;
126
+ margin-top: auto;
127
+ margin-bottom: auto;
128
+ line-height: 0;
129
+ right: 10px;
130
+ opacity: 1;
131
+ transition: opacity 0.25s ease;
132
+ }
133
+
134
+ .react-toggle--checked .react-toggle-track-x {
135
+ opacity: 0;
136
+ }
137
+
138
+ .react-toggle-thumb {
139
+ transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
140
+ position: absolute;
141
+ top: 1px;
142
+ left: 1px;
143
+ width: 22px;
144
+ height: 22px;
145
+ border: 1px solid #4d4d4d;
146
+ border-radius: 50%;
147
+ background-color: #fafafa;
148
+ box-sizing: border-box;
149
+ transition: all 0.25s ease;
150
+ }
151
+
152
+ .react-toggle--checked .react-toggle-thumb {
153
+ left: 27px;
154
+ border-color: var(--primary-color, #3b82f6);
155
+ }
156
+
157
+ .react-toggle--focus .react-toggle-thumb {
158
+ box-shadow: 0px 0px 3px 2px var(--primary-color, #3b82f6);
159
+ }
160
+
161
+ .react-toggle:active:not(.react-toggle--disabled) .react-toggle-thumb {
162
+ box-shadow: 0px 0px 5px 5px var(--primary-color, #3b82f6);
163
+ }
164
+ }
165
+
24
166
  .vs-datagrid--row:hover {
25
167
  .InovuaReactDataGrid__row-cell-wrap {
26
168
  background: var(--hover-color, rgb(230, 240, 250)) !important;
@@ -128,10 +270,9 @@
128
270
 
129
271
  .modalwrap {
130
272
  background: var(--tertiary-color);
131
- border-radius: 3px;
132
- overflow: visible;
133
- border-top-left-radius: var(--br);
134
- border-top-right-radius: var(--br);
273
+ border-radius: 8px;
274
+ overflow: hidden;
275
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
135
276
  }
136
277
 
137
278
  .top--modal {
@@ -146,17 +287,31 @@
146
287
 
147
288
  .modal__header {
148
289
  width: 100%;
149
- border-bottom: 1px solid rgba(var(--primary-rgb), 0.15);
290
+ border-bottom: 1px solid rgba(var(--primary-rgb), 0.1);
150
291
  display: flex;
151
292
  align-items: center;
152
293
  justify-content: space-between;
153
- padding: 0.75rem;
294
+ padding: 0.75rem 1rem;
295
+ background-color: #fff;
154
296
 
155
297
  h1 {
156
298
  color: var(--secondary-color);
157
299
  margin: 0;
158
300
  padding: 0;
159
- font-size: 1.15em;
301
+ font-size: 1.1em;
302
+ font-weight: 600;
303
+ display: flex;
304
+ align-items: center;
305
+ gap: 8px;
306
+
307
+ &::before {
308
+ content: '';
309
+ display: block;
310
+ width: 3px;
311
+ height: 16px;
312
+ background-color: var(--primary-color);
313
+ border-radius: 2px;
314
+ }
160
315
  }
161
316
  }
162
317
 
@@ -169,8 +324,15 @@
169
324
  outline: none;
170
325
  border: none;
171
326
  border-radius: var(--br);
172
- display: block;
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
173
330
  line-height: 1;
331
+ transition: all 0.2s ease;
332
+
333
+ &:hover {
334
+ background: rgba(var(--paragraph-rgb), 0.1);
335
+ }
174
336
 
175
337
  svg {
176
338
  color: var(--primary-color);
@@ -184,22 +346,315 @@
184
346
  padding: 0.75rem;
185
347
  max-height: 80vh;
186
348
  overflow-y: auto;
349
+
350
+ &--gallery {
351
+ padding: 0.75rem;
352
+ background-color: #f9fafb;
353
+ min-width: 600px; /* Ensure the gallery modal has a minimum width */
354
+ max-width: 90vw; /* Limit maximum width */
355
+ }
187
356
  }
188
357
 
189
358
  .formcontainer {
190
359
  width: 100%;
360
+ padding: 8px 0;
361
+
362
+ /* Custom styling for gallery container */
363
+ .galleryContainer {
364
+ margin-top: 8px;
365
+ }
366
+
367
+ /* Gallery instructions styling */
368
+ .galleryInstructions {
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 8px;
372
+ margin-bottom: 12px;
373
+ padding: 10px 12px;
374
+ background-color: #f0f7ff;
375
+ border: 1px solid rgba(var(--primary-rgb), 0.2);
376
+ border-radius: 6px;
377
+ color: var(--secondary-color);
378
+ font-size: 0.9rem;
379
+
380
+ svg {
381
+ color: var(--primary-color);
382
+ flex-shrink: 0;
383
+ }
384
+
385
+ span {
386
+ line-height: 1.4;
387
+ }
388
+ }
389
+
390
+ /* Gallery image overlay styling */
391
+ .galleryImageOverlay {
392
+ position: absolute;
393
+ top: 0;
394
+ left: 0;
395
+ right: 0;
396
+ bottom: 0;
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ background-color: rgba(0, 0, 0, 0.5);
401
+ color: white;
402
+ opacity: 0;
403
+ transition: opacity 0.3s ease;
404
+ border-radius: 6px;
405
+ z-index: 5;
406
+
407
+ span {
408
+ padding: 8px 14px;
409
+ background-color: rgba(0, 0, 0, 0.75);
410
+ border-radius: 4px;
411
+ font-size: 0.85rem;
412
+ font-weight: 500;
413
+ display: flex;
414
+ align-items: center;
415
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
416
+ border: 1px solid rgba(255, 255, 255, 0.1);
417
+
418
+ svg {
419
+ color: var(--primary-color, #3b82f6);
420
+ }
421
+ }
422
+ }
423
+
424
+ /* Custom grid layout for gallery */
425
+ .customGalleryGrid {
426
+ display: grid;
427
+ grid-template-columns: repeat(4, 1fr); /* 4 columns by default */
428
+ gap: 16px;
429
+ margin-top: 16px;
430
+
431
+ @media (max-width: 1200px) {
432
+ grid-template-columns: repeat(
433
+ 3,
434
+ 1fr
435
+ ); /* 3 columns on medium screens */
436
+ }
437
+
438
+ @media (max-width: 768px) {
439
+ grid-template-columns: repeat(
440
+ 2,
441
+ 1fr
442
+ ); /* 2 columns on small screens */
443
+ }
444
+
445
+ .galleryItem {
446
+ aspect-ratio: 4/3;
447
+ border-radius: 6px;
448
+ overflow: hidden;
449
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
450
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
451
+ cursor: pointer;
452
+ position: relative;
453
+ display: flex;
454
+ flex-direction: column;
455
+
456
+ &:hover {
457
+ transform: translateY(-2px);
458
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
459
+
460
+ .galleryImageOverlay {
461
+ opacity: 1;
462
+ }
463
+ }
464
+
465
+ img {
466
+ width: 100%;
467
+ height: 100%;
468
+ object-fit: cover;
469
+ display: block;
470
+ }
471
+
472
+ .imageCaption {
473
+ padding: 8px;
474
+ background-color: rgba(0, 0, 0, 0.6);
475
+ color: white;
476
+ font-size: 0.8rem;
477
+ position: absolute;
478
+ bottom: 0;
479
+ left: 0;
480
+ right: 0;
481
+ white-space: nowrap;
482
+ overflow: hidden;
483
+ text-overflow: ellipsis;
484
+ }
485
+ }
486
+ }
487
+
488
+ /* Add custom styles to override react-grid-gallery */
489
+ :global {
490
+ /* These styles will be applied globally */
491
+ .ReactGridGallery_tile {
492
+ border-radius: 6px !important;
493
+ overflow: hidden !important;
494
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
495
+ transition: transform 0.2s ease, box-shadow 0.2s ease !important;
496
+ cursor: pointer !important;
497
+ margin-bottom: 8px !important;
498
+ }
499
+
500
+ .ReactGridGallery_tile:hover {
501
+ transform: translateY(-2px) !important;
502
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
503
+
504
+ /* Show overlay on hover */
505
+ .galleryImageOverlay {
506
+ opacity: 1 !important;
507
+ }
508
+ }
509
+
510
+ .ReactGridGallery_tile-viewport {
511
+ border-radius: 6px !important;
512
+ overflow: hidden !important;
513
+ }
514
+
515
+ .ReactGridGallery_tile-description {
516
+ background: rgba(0, 0, 0, 0.6) !important;
517
+ padding: 8px !important;
518
+ font-size: 0.85rem !important;
519
+ }
520
+ }
191
521
  }
192
522
 
193
523
  .noDataMessage {
194
524
  text-align: center;
195
525
  color: var(--secondary-color);
196
- font-size: 1.125em;
197
- padding: 2rem;
198
- margin: 2rem auto;
526
+ font-size: 1rem;
527
+ padding: 1.5rem;
528
+ margin: 1rem auto;
199
529
  background: var(--tertiary-color);
200
530
  border: 1px solid rgba(var(--primary-rgb), 0.15);
201
531
  border-radius: var(--br);
202
- max-width: 80%;
203
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
532
+ max-width: 90%;
533
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
204
534
  font-style: italic;
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ gap: 8px;
539
+
540
+ svg {
541
+ color: var(--primary-color);
542
+ opacity: 0.7;
543
+ }
544
+ }
545
+
546
+ .tdaction {
547
+ cursor: pointer;
548
+ color: #6b7280;
549
+
550
+ &:hover {
551
+ color: var(--primary-color, #3b82f6);
552
+ }
553
+ }
554
+
555
+ .uploadSection {
556
+ margin-bottom: 16px;
557
+ border-bottom: 1px solid #e5e7eb;
558
+ padding-bottom: 16px;
559
+
560
+ h3 {
561
+ font-size: 0.95rem;
562
+ font-weight: 600;
563
+ margin-bottom: 8px;
564
+ color: var(--secondary-color, #111827);
565
+ display: flex;
566
+ align-items: center;
567
+ gap: 6px;
568
+
569
+ svg {
570
+ color: var(--primary-color, #3b82f6);
571
+ }
572
+ }
573
+ }
574
+
575
+ .dropzoneContainer {
576
+ position: relative;
577
+ margin-bottom: 12px;
578
+ }
579
+
580
+ .progressBar {
581
+ height: 3px;
582
+ background-color: #e5e7eb;
583
+ border-radius: 2px;
584
+ overflow: hidden;
585
+ margin-bottom: 8px;
586
+ }
587
+
588
+ .progressBarInner {
589
+ height: 100%;
590
+ background-color: var(--primary-color, #3b82f6);
591
+ transition: width 0.3s ease;
592
+ }
593
+
594
+ .dropzone {
595
+ border: 1px dashed #d1d5db;
596
+ border-radius: 6px;
597
+ padding: 12px;
598
+ text-align: center;
599
+ cursor: pointer;
600
+ transition: all 0.2s ease;
601
+ display: flex;
602
+ flex-direction: row;
603
+ align-items: center;
604
+ justify-content: center;
605
+ gap: 10px;
606
+
607
+ &:hover {
608
+ border-color: var(--primary-color, #3b82f6);
609
+ background-color: rgba(59, 130, 246, 0.05);
610
+ }
611
+
612
+ svg {
613
+ color: var(--primary-color, #3b82f6);
614
+ flex-shrink: 0;
615
+ }
616
+
617
+ p {
618
+ margin: 0;
619
+ color: #6b7280;
620
+ font-size: 0.9rem;
621
+ }
622
+ }
623
+
624
+ .filterInput {
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: flex-start;
628
+ margin-bottom: 10px;
629
+ width: 100%;
630
+
631
+ &--alt {
632
+ display: flex;
633
+ align-items: center;
634
+ justify-content: flex-start;
635
+ margin-bottom: 10px;
636
+ width: 100%;
637
+ height: 58px;
638
+
639
+ .icon-search {
640
+ position: absolute;
641
+ left: 10px;
642
+ color: #6b7280;
643
+ }
644
+
645
+ input {
646
+ height: 38px;
647
+ border-radius: 4px;
648
+ border: 1px solid #d1d5db;
649
+ padding: 0 10px 0 35px;
650
+ width: 100%;
651
+ max-width: 300px;
652
+
653
+ &:focus {
654
+ outline: none;
655
+ border-color: var(--primary-color, #3b82f6);
656
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
657
+ }
658
+ }
659
+ }
205
660
  }
@@ -378,6 +378,144 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
378
378
  z-index: -1 !important;
379
379
  }
380
380
 
381
+ /* React Toggle Styling */
382
+ .react-toggle {
383
+ touch-action: pan-x;
384
+ display: inline-block;
385
+ position: relative;
386
+ cursor: pointer;
387
+ background-color: transparent;
388
+ border: 0;
389
+ padding: 0;
390
+ -webkit-touch-callout: none;
391
+ -webkit-user-select: none;
392
+ -khtml-user-select: none;
393
+ -moz-user-select: none;
394
+ -ms-user-select: none;
395
+ user-select: none;
396
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
397
+ -webkit-tap-highlight-color: transparent;
398
+ z-index: 2; /* Ensure toggle is above checkbox */
399
+ }
400
+
401
+ /* Fix for specific toggle in forms */
402
+ form div:has(span:contains('Use T-Bar?')) .react-toggle {
403
+ margin-right: 8px;
404
+ vertical-align: middle;
405
+ }
406
+
407
+ /* Hide checkboxes when they're next to a toggle */
408
+ .react-toggle + input[type='checkbox'],
409
+ input[type='checkbox'] + .react-toggle,
410
+ label:has(.react-toggle) input[type='checkbox'],
411
+ div:has(.react-toggle) > input[type='checkbox'],
412
+ form div:has(.react-toggle) input[type='checkbox'] {
413
+ display: none !important;
414
+ visibility: hidden !important;
415
+ opacity: 0 !important;
416
+ position: absolute !important;
417
+ pointer-events: none !important;
418
+ width: 0 !important;
419
+ height: 0 !important;
420
+ }
421
+
422
+ .react-toggle-screenreader-only {
423
+ border: 0;
424
+ clip: rect(0 0 0 0);
425
+ height: 1px;
426
+ margin: -1px;
427
+ overflow: hidden;
428
+ padding: 0;
429
+ position: absolute;
430
+ width: 1px;
431
+ }
432
+
433
+ .react-toggle-track {
434
+ width: 50px;
435
+ height: 24px;
436
+ padding: 0;
437
+ border-radius: 30px;
438
+ background-color: #4d4d4d;
439
+ transition: all 0.2s ease;
440
+ }
441
+
442
+ .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
443
+ background-color: #000000;
444
+ }
445
+
446
+ .react-toggle--checked .react-toggle-track {
447
+ background-color: var(--primary-color, #3b82f6);
448
+ }
449
+
450
+ .react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
451
+ background-color: var(--primary-color, #3b82f6);
452
+ opacity: 0.8;
453
+ }
454
+
455
+ .react-toggle-track-check {
456
+ position: absolute;
457
+ width: 14px;
458
+ height: 10px;
459
+ top: 0px;
460
+ bottom: 0px;
461
+ margin-top: auto;
462
+ margin-bottom: auto;
463
+ line-height: 0;
464
+ left: 8px;
465
+ opacity: 0;
466
+ transition: opacity 0.25s ease;
467
+ }
468
+
469
+ .react-toggle--checked .react-toggle-track-check {
470
+ opacity: 1;
471
+ transition: opacity 0.25s ease;
472
+ }
473
+
474
+ .react-toggle-track-x {
475
+ position: absolute;
476
+ width: 10px;
477
+ height: 10px;
478
+ top: 0px;
479
+ bottom: 0px;
480
+ margin-top: auto;
481
+ margin-bottom: auto;
482
+ line-height: 0;
483
+ right: 10px;
484
+ opacity: 1;
485
+ transition: opacity 0.25s ease;
486
+ }
487
+
488
+ .react-toggle--checked .react-toggle-track-x {
489
+ opacity: 0;
490
+ }
491
+
492
+ .react-toggle-thumb {
493
+ transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
494
+ position: absolute;
495
+ top: 1px;
496
+ left: 1px;
497
+ width: 22px;
498
+ height: 22px;
499
+ border: 1px solid #4d4d4d;
500
+ border-radius: 50%;
501
+ background-color: #fafafa;
502
+ box-sizing: border-box;
503
+ transition: all 0.25s ease;
504
+ }
505
+
506
+ .react-toggle--checked .react-toggle-thumb {
507
+ left: 27px;
508
+ border-color: var(--primary-color, #3b82f6);
509
+ }
510
+
511
+ .react-toggle--focus .react-toggle-thumb {
512
+ box-shadow: 0px 0px 3px 2px var(--primary-color, #3b82f6);
513
+ }
514
+
515
+ .react-toggle:active:not(.react-toggle--disabled) .react-toggle-thumb {
516
+ box-shadow: 0px 0px 5px 5px var(--primary-color, #3b82f6);
517
+ }
518
+
381
519
  /* Safari-specific fixes for form elements */
382
520
  @supports (-webkit-touch-callout: none) {
383
521
  /* Safari-specific styles */