@visns-studio/visns-components 5.12.5 → 5.12.7

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
@@ -24,13 +24,13 @@
24
24
  "dayjs": "^1.11.13",
25
25
  "fabric": "^6.7.0",
26
26
  "file-saver": "^2.0.5",
27
- "framer-motion": "^12.19.2",
27
+ "framer-motion": "^12.23.0",
28
28
  "html-react-parser": "^5.2.5",
29
29
  "lodash": "^4.17.21",
30
30
  "lodash.debounce": "^4.0.8",
31
- "lucide-react": "^0.518.0",
31
+ "lucide-react": "^0.525.0",
32
32
  "moment": "^2.30.1",
33
- "motion": "^12.19.2",
33
+ "motion": "^12.23.0",
34
34
  "numeral": "^2.0.6",
35
35
  "pluralize": "^8.0.0",
36
36
  "qrcode.react": "^4.2.0",
@@ -47,10 +47,10 @@
47
47
  "react-reveal": "^1.2.2",
48
48
  "react-router-dom": "^6.30.1",
49
49
  "react-select": "^5.10.1",
50
- "react-signature-pad-wrapper": "^4.1.0",
50
+ "react-signature-pad-wrapper": "^4.1.1",
51
51
  "react-slugify": "^4.0.1",
52
52
  "react-sortable-hoc": "^2.0.0",
53
- "react-to-print": "^3.1.0",
53
+ "react-to-print": "^3.1.1",
54
54
  "react-toastify": "^11.0.5",
55
55
  "react-toggle": "^4.1.3",
56
56
  "react-tooltip": "^5.29.1",
@@ -58,19 +58,19 @@
58
58
  "reactjs-popup": "^2.0.6",
59
59
  "style-loader": "^4.0.0",
60
60
  "swapy": "^1.0.5",
61
- "sweetalert2": "^11.22.1",
61
+ "sweetalert2": "^11.22.2",
62
62
  "tesseract.js": "^6.0.1",
63
63
  "truncate": "^3.0.0",
64
64
  "uuid": "^11.1.0",
65
65
  "validator": "^13.15.15",
66
66
  "vite": "^6.3.5",
67
67
  "yarn": "^1.22.22",
68
- "yet-another-react-lightbox": "^3.23.4"
68
+ "yet-another-react-lightbox": "^3.24.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@babel/core": "^7.27.7",
72
- "@babel/plugin-transform-runtime": "^7.27.4",
73
- "@babel/preset-env": "^7.27.2",
71
+ "@babel/core": "^7.28.0",
72
+ "@babel/plugin-transform-runtime": "^7.28.0",
73
+ "@babel/preset-env": "^7.28.0",
74
74
  "@babel/preset-react": "^7.27.1",
75
75
  "babel-loader": "^10.0.0",
76
76
  "copy-webpack-plugin": "^13.0.0",
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.12.5",
90
+ "version": "5.12.7",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -37,6 +37,7 @@ import Tools from './sketch/tools';
37
37
  import VisnsAsyncSelect from './AsyncSelect';
38
38
  import VisnsAutocomplete from './Autocomplete';
39
39
  import VisnsDropZone from './DropZone';
40
+ import DatePickerPortal from './utils/DatePickerPortal';
40
41
 
41
42
  import 'react-toggle/style.css';
42
43
  import 'react-datepicker/dist/react-datepicker.css';
@@ -72,6 +73,7 @@ function Field({
72
73
  }) {
73
74
  const editorRef = useRef(null);
74
75
  const fileImageInputRef = useRef(null);
76
+ const containerRef = useRef(null);
75
77
  const [autoValue, setAutoValue] = useState('');
76
78
  const [colorPickerShow, setColorPickerShow] = useState(false);
77
79
  const [counter, setCounter] = useState(0);
@@ -2129,31 +2131,38 @@ function Field({
2129
2131
  case 'date':
2130
2132
  case 'datetime':
2131
2133
  return (
2132
- <DatePicker
2133
- dateFormat={
2134
- settings.format
2135
- ? settings.format
2136
- : settings.type === 'datetime'
2137
- ? 'dd-MM-yyyy hh:mm aa'
2138
- : 'dd-MM-yyyy'
2139
- }
2140
- isClearable
2141
- className={inputClass[settings.id]}
2142
- selected={inputValue || ''}
2143
- showMonthDropdown
2144
- showYearDropdown
2145
- scrollableYearDropdown
2146
- onChange={(date) => onChangeDate(date, settings.id)}
2147
- showTimeSelect={settings.type === 'datetime'}
2148
- showTimeSelectOnly={settings.type === 'time'}
2149
- timeIntervals={
2150
- settings.type === 'time' ? 15 : undefined
2151
- }
2152
- timeCaption={
2153
- settings.type === 'time' ? 'Time' : undefined
2154
- }
2155
- shouldCloseOnSelect={true}
2156
- />
2134
+ <DatePickerPortal
2135
+ containerRef={containerRef}
2136
+ centerScreen={settings.centerScreen}
2137
+ modalContext={settings.modalContext}
2138
+ usePortal={settings.usePortal}
2139
+ >
2140
+ <DatePicker
2141
+ dateFormat={
2142
+ settings.format
2143
+ ? settings.format
2144
+ : settings.type === 'datetime'
2145
+ ? 'dd-MM-yyyy hh:mm aa'
2146
+ : 'dd-MM-yyyy'
2147
+ }
2148
+ isClearable
2149
+ className={inputClass[settings.id]}
2150
+ selected={inputValue || ''}
2151
+ showMonthDropdown
2152
+ showYearDropdown
2153
+ scrollableYearDropdown
2154
+ onChange={(date) => onChangeDate(date, settings.id)}
2155
+ showTimeSelect={settings.type === 'datetime'}
2156
+ showTimeSelectOnly={settings.type === 'time'}
2157
+ timeIntervals={
2158
+ settings.type === 'time' ? 15 : undefined
2159
+ }
2160
+ timeCaption={
2161
+ settings.type === 'time' ? 'Time' : undefined
2162
+ }
2163
+ shouldCloseOnSelect={true}
2164
+ />
2165
+ </DatePickerPortal>
2157
2166
  );
2158
2167
  case 'plaintext':
2159
2168
  if (settings.description) {
@@ -2254,21 +2263,28 @@ function Field({
2254
2263
  );
2255
2264
  case 'time':
2256
2265
  return (
2257
- <DatePicker
2258
- dateFormat="hh:mm aa"
2259
- isClearable
2260
- className={inputClass[settings.id]}
2261
- selected={inputValue || ''}
2262
- showMonthDropdown
2263
- showYearDropdown
2264
- scrollableYearDropdown
2265
- onChange={(date) => onChangeDate(date, settings.id)}
2266
- showTimeSelect={true}
2267
- showTimeSelectOnly={true}
2268
- timeIntervals={15}
2269
- timeCaption={'Time'}
2270
- shouldCloseOnSelect={true}
2271
- />
2266
+ <DatePickerPortal
2267
+ containerRef={containerRef}
2268
+ centerScreen={settings.centerScreen}
2269
+ modalContext={settings.modalContext}
2270
+ usePortal={settings.usePortal}
2271
+ >
2272
+ <DatePicker
2273
+ dateFormat="hh:mm aa"
2274
+ isClearable
2275
+ className={inputClass[settings.id]}
2276
+ selected={inputValue || ''}
2277
+ showMonthDropdown
2278
+ showYearDropdown
2279
+ scrollableYearDropdown
2280
+ onChange={(date) => onChangeDate(date, settings.id)}
2281
+ showTimeSelect={true}
2282
+ showTimeSelectOnly={true}
2283
+ timeIntervals={15}
2284
+ timeCaption={'Time'}
2285
+ shouldCloseOnSelect={true}
2286
+ />
2287
+ </DatePickerPortal>
2272
2288
  );
2273
2289
  case 'toggle':
2274
2290
  return (
@@ -2522,7 +2538,7 @@ function Field({
2522
2538
  }, [counter]);
2523
2539
 
2524
2540
  return (
2525
- <div className={formItemClass} style={formItemStyle}>
2541
+ <div className={formItemClass} style={formItemStyle} ref={containerRef}>
2526
2542
  <label
2527
2543
  className={
2528
2544
  ['richeditor', 'textarea'].includes(settings.type)
@@ -54,10 +54,16 @@ import {
54
54
  Eye as EyeOpen,
55
55
  EyeOff as EyeSlashed,
56
56
  File,
57
+ FileText,
58
+ FileSpreadsheet,
59
+ FileImage,
60
+ FileCode,
61
+ FileArchive,
57
62
  Edit as Pencil,
58
63
  Trash2 as TrashCan,
64
+ X as CircleX,
59
65
  } from 'lucide-react';
60
- import { confirmDialog } from '../../utils/ConfirmDialog';
66
+ import { confirmDialog } from '../utils/ConfirmDialog';
61
67
 
62
68
  import 'react-big-calendar/lib/css/react-big-calendar.css';
63
69
  import 'react-big-calendar/lib/addons/dragAndDrop/styles.css';
@@ -79,6 +85,9 @@ import TableFilter from '../TableFilter';
79
85
  import ProposalTemplateSectionManager from '../proposal/ProposalTemplateSectionManager';
80
86
  import ProposalTemplatePreview from '../proposal/ProposalTemplatePreview';
81
87
 
88
+ // Gallery Modal
89
+ import GalleryModal from '../modals/GalleryModal';
90
+
82
91
  import styles from '../styles/GenericDetail.module.scss';
83
92
 
84
93
  const localizer = momentLocalizer(moment);
@@ -127,6 +136,88 @@ const optimizeImageIfNeeded = async (file) => {
127
136
  return file;
128
137
  };
129
138
 
139
+ // Helper function to get accept types based on filetype configuration
140
+ const getAcceptTypes = (filetype) => {
141
+ switch (filetype) {
142
+ case 'document':
143
+ return {
144
+ 'application/pdf': ['.pdf'],
145
+ 'application/msword': ['.doc'],
146
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
147
+ 'application/vnd.ms-excel': ['.xls'],
148
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
149
+ 'application/vnd.ms-powerpoint': ['.ppt'],
150
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
151
+ 'text/plain': ['.txt'],
152
+ 'text/csv': ['.csv'],
153
+ 'application/zip': ['.zip'],
154
+ 'application/x-rar-compressed': ['.rar'],
155
+ 'application/x-7z-compressed': ['.7z']
156
+ };
157
+ case 'image':
158
+ return {
159
+ 'image/jpeg': ['.jpg', '.jpeg'],
160
+ 'image/png': ['.png'],
161
+ 'image/gif': ['.gif'],
162
+ 'image/webp': ['.webp'],
163
+ 'image/svg+xml': ['.svg'],
164
+ 'image/bmp': ['.bmp'],
165
+ 'image/tiff': ['.tiff', '.tif']
166
+ };
167
+ default:
168
+ return undefined; // Accept all file types
169
+ }
170
+ };
171
+
172
+ // Helper function to get the appropriate icon based on file extension
173
+ const getFileIcon = (filename) => {
174
+ const extension = filename.split('.').pop().toLowerCase();
175
+
176
+ switch (extension) {
177
+ case 'pdf':
178
+ return <FileText strokeWidth={2} size={20} color="#dc2626" />;
179
+ case 'doc':
180
+ case 'docx':
181
+ return <FileText strokeWidth={2} size={20} color="#2563eb" />;
182
+ case 'xls':
183
+ case 'xlsx':
184
+ return <FileSpreadsheet strokeWidth={2} size={20} color="#16a34a" />;
185
+ case 'ppt':
186
+ case 'pptx':
187
+ return <FileText strokeWidth={2} size={20} color="#ea580c" />;
188
+ case 'txt':
189
+ case 'csv':
190
+ return <FileCode strokeWidth={2} size={20} color="#6b7280" />;
191
+ case 'zip':
192
+ case 'rar':
193
+ case '7z':
194
+ return <FileArchive strokeWidth={2} size={20} color="#7c3aed" />;
195
+ case 'jpg':
196
+ case 'jpeg':
197
+ case 'png':
198
+ case 'gif':
199
+ case 'webp':
200
+ case 'svg':
201
+ case 'bmp':
202
+ case 'tiff':
203
+ case 'tif':
204
+ return <FileImage strokeWidth={2} size={20} color="#059669" />;
205
+ default:
206
+ return <File strokeWidth={2} size={20} />;
207
+ }
208
+ };
209
+
210
+ // Helper function to format file size
211
+ const formatFileSize = (bytes) => {
212
+ if (!bytes || bytes === 0) return '';
213
+
214
+ const k = 1024;
215
+ const sizes = ['B', 'KB', 'MB', 'GB'];
216
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
217
+
218
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
219
+ };
220
+
130
221
  function GenericDetail({
131
222
  extraUrlParam,
132
223
  layout = 'full',
@@ -158,6 +249,19 @@ function GenericDetail({
158
249
  const [total, setTotal] = useState(0);
159
250
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
160
251
 
252
+ /** Gallery Modal States */
253
+ const [modalGalleryShow, setModalGalleryShow] = useState(false);
254
+ const [galleryData, setGalleryData] = useState([]);
255
+ const [currentItem, setCurrentItem] = useState(null);
256
+ const [lightboxOpen, setLightboxOpen] = useState(false);
257
+ const [lightboxIndex, setLightboxIndex] = useState(0);
258
+
259
+ // Gallery modal close function
260
+ const modalGalleryClose = () => {
261
+ setModalGalleryShow(false);
262
+ setLightboxOpen(false);
263
+ };
264
+
161
265
  /** Stage Popup States */
162
266
  const [showStagePopup, setShowStagePopup] = useState(false);
163
267
  const [stagePopupData, setStagePopupData] = useState(null);
@@ -1445,7 +1549,7 @@ function GenericDetail({
1445
1549
  message:
1446
1550
  'Are you sure you want to delete ' +
1447
1551
  name,
1448
- data: { name: name }, // Add context data
1552
+ data: { name: name },
1449
1553
  buttons: [
1450
1554
  {
1451
1555
  label: 'Yes',
@@ -1473,7 +1577,9 @@ function GenericDetail({
1473
1577
  },
1474
1578
  {
1475
1579
  label: 'No',
1476
- onClick: () => close(),
1580
+ onClick: () => {
1581
+ // Do nothing on cancel
1582
+ },
1477
1583
  },
1478
1584
  ],
1479
1585
  });
@@ -1501,6 +1607,7 @@ function GenericDetail({
1501
1607
  />
1502
1608
  </div>
1503
1609
  <Dropzone
1610
+ accept={getAcceptTypes(activeTabConfig.filetype)}
1504
1611
  onDrop={(acceptedFiles) => {
1505
1612
  const uploadFile = async (file) => {
1506
1613
  // Optimize image if it's an image file
@@ -1695,20 +1802,9 @@ function GenericDetail({
1695
1802
  styles.sortlist
1696
1803
  }
1697
1804
  >
1698
- {data[
1699
- activeTabConfig
1700
- .key
1701
- ] &&
1702
- Array.isArray(
1703
- data[
1704
- activeTabConfig
1705
- .key
1706
- ]
1707
- ) &&
1708
- data[
1709
- activeTabConfig
1710
- .key
1711
- ].map(
1805
+ {files &&
1806
+ Array.isArray(files) &&
1807
+ files.map(
1712
1808
  (a, b) => {
1713
1809
  const imageSource =
1714
1810
  a.base64_encoded_image ||
@@ -1732,6 +1828,33 @@ function GenericDetail({
1732
1828
  alt={
1733
1829
  a.file_name
1734
1830
  }
1831
+ onClick={(e) => {
1832
+ e.stopPropagation();
1833
+ e.preventDefault();
1834
+
1835
+ // Prepare gallery data in the format expected by Lightbox
1836
+ const galleryArray = files
1837
+ .filter(file => {
1838
+ const imageSource = file.base64_encoded_image || file.s3_url || file.file_url;
1839
+ return imageSource;
1840
+ })
1841
+ .map((file) => ({
1842
+ src: file.base64_encoded_image || file.s3_url || file.file_url,
1843
+ caption: file.file_name,
1844
+ width: 320,
1845
+ height: 240,
1846
+ }));
1847
+
1848
+ // Set gallery data for lightbox
1849
+ setGalleryData(galleryArray);
1850
+
1851
+ // Set initial lightbox index
1852
+ setLightboxIndex(b);
1853
+
1854
+ // Open lightbox directly
1855
+ setLightboxOpen(true);
1856
+ }}
1857
+ style={{ cursor: 'pointer' }}
1735
1858
  />
1736
1859
  <TrashCan
1737
1860
  onClick={(
@@ -1788,18 +1911,16 @@ function GenericDetail({
1788
1911
  );
1789
1912
  }}
1790
1913
  >
1791
- <File
1792
- strokeWidth={
1793
- 2
1794
- }
1795
- size={
1796
- 20
1797
- }
1798
- />
1914
+ {getFileIcon(a.file_name)}
1799
1915
  <div className="filename">
1800
- {
1801
- a.file_name
1802
- }
1916
+ <div className="file-name">
1917
+ {a.file_name}
1918
+ </div>
1919
+ {a.file_size && (
1920
+ <div className="file-size">
1921
+ {formatFileSize(a.file_size)}
1922
+ </div>
1923
+ )}
1803
1924
  </div>
1804
1925
  </div>
1805
1926
  <button
@@ -2390,6 +2511,7 @@ function GenericDetail({
2390
2511
  }
2391
2512
  >
2392
2513
  <Dropzone
2514
+ accept={getAcceptTypes(activeTabConfig.filetype)}
2393
2515
  onDrop={(
2394
2516
  acceptedFiles
2395
2517
  ) => {
@@ -2718,6 +2840,17 @@ function GenericDetail({
2718
2840
  }
2719
2841
  }, [subnav, tabs]);
2720
2842
 
2843
+ // Update files when active tab changes for dropzone tabs
2844
+ useEffect(() => {
2845
+ if (activeTabConfig && activeTabConfig.type === 'dropzone' && data) {
2846
+ if (activeTabConfig.file_relationship && data[activeTabConfig.file_relationship]) {
2847
+ setFiles(data[activeTabConfig.file_relationship]);
2848
+ } else if (data.files) {
2849
+ setFiles(data.files);
2850
+ }
2851
+ }
2852
+ }, [activeTabConfig, data]);
2853
+
2721
2854
  useEffect(() => {
2722
2855
  const fetchDropdownOptions = async () => {
2723
2856
  if (activeTabConfig?.columns) {
@@ -3167,6 +3300,20 @@ function GenericDetail({
3167
3300
  entityData={data}
3168
3301
  routeParams={routeParams}
3169
3302
  />
3303
+
3304
+ {/* Gallery Modal for image viewing */}
3305
+ <GalleryModal
3306
+ modalGalleryShow={modalGalleryShow}
3307
+ modalGalleryClose={modalGalleryClose}
3308
+ galleryData={galleryData}
3309
+ setGalleryData={setGalleryData}
3310
+ currentItem={currentItem}
3311
+ form={{}}
3312
+ lightboxOpen={lightboxOpen}
3313
+ setLightboxOpen={setLightboxOpen}
3314
+ lightboxIndex={lightboxIndex}
3315
+ setLightboxIndex={setLightboxIndex}
3316
+ />
3170
3317
  </>
3171
3318
  );
3172
3319
  }
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
4
4
  import imageCompression from 'browser-image-compression';
5
5
  import Vapor from 'laravel-vapor';
6
6
  import Lightbox from 'yet-another-react-lightbox';
7
+ import 'yet-another-react-lightbox/styles.css';
7
8
  import { X, Image as ImageIcon, CloudUpload } from 'lucide-react';
8
9
  import styles from '../styles/DataGrid.module.scss';
9
10
  import CustomFetch from '../Fetch';
@@ -396,7 +397,34 @@ const GalleryModal = ({
396
397
  open={lightboxOpen}
397
398
  close={() => setLightboxOpen(false)}
398
399
  index={lightboxIndex}
399
- slides={galleryData.map((img) => ({ src: img.src }))}
400
+ slides={galleryData.map((img, index) => ({
401
+ src: img.src,
402
+ alt: img.caption || `Image ${index + 1}`,
403
+ title: img.caption
404
+ }))}
405
+ styles={{
406
+ container: {
407
+ backgroundColor: "rgba(0, 0, 0, 0.9)",
408
+ zIndex: 999999
409
+ }
410
+ }}
411
+ controller={{
412
+ closeOnBackdropClick: true,
413
+ closeOnPullUp: true,
414
+ closeOnPullDown: true
415
+ }}
416
+ carousel={{
417
+ finite: true,
418
+ preload: 1,
419
+ padding: "16px",
420
+ spacing: "30%",
421
+ imageFit: "contain"
422
+ }}
423
+ animation={{
424
+ fade: 300,
425
+ swipe: 400
426
+ }}
427
+ portal={{ root: document.body }}
400
428
  />
401
429
  </>
402
430
  );
@@ -333,9 +333,25 @@
333
333
  }
334
334
 
335
335
  .filename {
336
- white-space: nowrap;
336
+ display: flex;
337
+ flex-direction: column;
337
338
  overflow: hidden;
338
- text-overflow: ellipsis;
339
+ flex: 1;
340
+
341
+ .file-name {
342
+ white-space: nowrap;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ font-weight: 500;
346
+ line-height: 1.2;
347
+ }
348
+
349
+ .file-size {
350
+ font-size: 0.75rem;
351
+ color: rgba(var(--primary-rgb), 0.7);
352
+ margin-top: 2px;
353
+ font-weight: 400;
354
+ }
339
355
  }
340
356
  }
341
357
 
@@ -364,26 +380,25 @@
364
380
  }
365
381
 
366
382
  .sortlist {
367
- display: grid;
368
- grid-template-columns: repeat(4, 1fr); /* 4 columns by default */
369
- gap: 16px;
383
+ display: grid !important;
384
+ grid-template-columns: repeat(4, 1fr) !important; /* 4 columns by default */
385
+ gap: 16px !important;
370
386
  padding: 16px 0;
387
+ margin: 0;
371
388
  list-style: none;
389
+ box-sizing: border-box;
372
390
 
373
391
  /* Responsive grid layout */
374
392
  @media (max-width: 1200px) {
375
- grid-template-columns: repeat(3, 1fr); /* 3 columns on medium screens */
393
+ grid-template-columns: repeat(3, 1fr) !important; /* 3 columns on medium screens */
376
394
  }
377
395
 
378
396
  @media (max-width: 768px) {
379
- grid-template-columns: repeat(2, 1fr); /* 2 columns on small screens */
397
+ grid-template-columns: repeat(2, 1fr) !important; /* 2 columns on small screens */
380
398
  }
381
399
 
382
400
  @media (max-width: 480px) {
383
- grid-template-columns: repeat(
384
- 1,
385
- 1fr
386
- ); /* 1 column on very small screens */
401
+ grid-template-columns: repeat(1, 1fr) !important; /* 1 column on very small screens */
387
402
  }
388
403
 
389
404
  li {
@@ -413,6 +428,12 @@
413
428
  width: 100%;
414
429
  height: 100%;
415
430
  object-fit: cover;
431
+ cursor: pointer;
432
+ transition: opacity 0.2s ease;
433
+
434
+ &:hover {
435
+ opacity: 0.9;
436
+ }
416
437
  }
417
438
 
418
439
  .delete {
@@ -819,3 +840,4 @@
819
840
  overflow-x: auto;
820
841
  white-space: pre-wrap;
821
842
  }
843
+
@@ -451,15 +451,7 @@ select:not(:placeholder-shown) + .fi__span {
451
451
  width: 100% !important;
452
452
  }
453
453
 
454
- .react-datepicker-popper {
455
- z-index: 99999 !important;
456
- position: fixed !important;
457
- transform: none !important;
458
- top: 50% !important;
459
- left: 50% !important;
460
- margin-top: -150px !important;
461
- margin-left: -150px !important;
462
- }
454
+ // DatePicker positioning removed - handled by global.css
463
455
 
464
456
  .react-datepicker__close-icon:after {
465
457
  background-color: rgba(var(--primary-rgb), 0.45) !important;
@@ -828,15 +828,7 @@ select:not(:placeholder-shown) + .fi__span {
828
828
  width: 100% !important;
829
829
  }
830
830
 
831
- .react-datepicker-popper {
832
- z-index: 99999 !important;
833
- position: fixed !important;
834
- transform: none !important;
835
- top: 50% !important;
836
- left: 50% !important;
837
- margin-top: -150px !important;
838
- margin-left: -150px !important;
839
- }
831
+ // DatePicker positioning removed - handled by global.css
840
832
 
841
833
  .react-datepicker__close-icon:after {
842
834
  background-color: rgba(var(--primary-rgb), 0.45) !important;
@@ -196,7 +196,7 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
196
196
  .inovua-react-toolkit-text-input__input
197
197
  ):not(.inovua-react-toolkit-numeric-input__input):not(
198
198
  .variable-inserter-search-input
199
- ) {
199
+ ):not(.react-datepicker__month-select):not(.react-datepicker__year-select) {
200
200
  background: white;
201
201
  border-radius: var(--br);
202
202
  border: 1px solid rgba(var(--paragraph-color-rgb), 0.15);
@@ -339,29 +339,38 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
339
339
  z-index: 999 !important;
340
340
  }
341
341
 
342
- /* DatePicker z-index fix */
342
+ /* DatePicker proper z-index hierarchy */
343
343
  .react-datepicker-popper {
344
- z-index: 99999 !important;
344
+ z-index: 1060 !important; /* Above Bootstrap modals (1050) */
345
+ position: absolute !important; /* Use absolute positioning for proper context */
346
+ }
347
+
348
+ /* Center-screen positioning for modal contexts */
349
+ .react-datepicker-popper.center-screen {
345
350
  position: fixed !important;
346
- transform: none !important;
347
351
  top: 50% !important;
348
352
  left: 50% !important;
349
- margin-top: -150px !important;
350
- margin-left: -150px !important;
353
+ transform: translate(-50%, -50%) !important;
354
+ z-index: 1060 !important;
355
+ }
356
+
357
+ /* Portal mode for modal contexts */
358
+ .react-datepicker-popper.portal-mode {
359
+ position: fixed !important;
360
+ z-index: 1060 !important;
351
361
  }
352
362
 
353
363
  .react-datepicker {
354
- position: absolute !important;
364
+ position: relative !important;
355
365
  display: inline-block !important;
356
- z-index: 99999 !important;
366
+ z-index: 1 !important;
357
367
  background-color: white !important;
358
368
  border-radius: 0.3rem !important;
359
369
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important;
360
370
  }
361
371
 
362
372
  .react-datepicker-portal {
363
- position: fixed !important;
364
- z-index: 99999 !important;
373
+ z-index: 1060 !important;
365
374
  }
366
375
 
367
376
  /* Fix for specific modal header that's causing issues */
@@ -369,12 +378,9 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
369
378
  z-index: 1 !important;
370
379
  }
371
380
 
372
- /* Make sure popup content doesn't constrain the datepicker */
373
- .popup-content {
374
- overflow: visible !important;
375
- }
376
-
377
- .modal__content {
381
+ /* Conditional overflow visibility only for modal contexts */
382
+ .modal-with-datepicker .popup-content,
383
+ .modal-with-datepicker .modal__content {
378
384
  overflow: visible !important;
379
385
  }
380
386
 
@@ -384,26 +390,179 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
384
390
  border: 1px solid #aeaeae !important;
385
391
  border-radius: 0.3rem !important;
386
392
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
393
+ float: left !important;
394
+ }
395
+
396
+ .react-datepicker {
397
+ position: relative !important;
398
+ display: inline-block !important;
399
+ background-color: white !important;
400
+ border-radius: 0.3rem !important;
401
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important;
402
+ min-width: 385px !important; /* Wider to accommodate month/year dropdowns */
403
+ width: auto !important;
404
+ }
405
+
406
+ /* Ensure time container appears on the right side */
407
+ .react-datepicker__time-container {
408
+ float: right !important;
409
+ border-left: 1px solid #aeaeae !important;
410
+ width: 85px !important;
411
+ display: block !important;
412
+ }
413
+
414
+ /* Fix navigation button positioning */
415
+ .react-datepicker__navigation {
416
+ position: absolute !important;
417
+ top: 2px !important;
418
+ z-index: 1 !important;
419
+ height: 32px !important;
420
+ width: 32px !important;
421
+ cursor: pointer !important;
422
+ }
423
+
424
+ .react-datepicker__navigation--previous {
425
+ left: 2px !important;
426
+ }
427
+
428
+ .react-datepicker__navigation--next {
429
+ right: 2px !important;
430
+ }
431
+
432
+ /* Fix next button positioning when time selector is present */
433
+ .react-datepicker__navigation--next--with-time:not(
434
+ .react-datepicker__navigation--next--with-today-button
435
+ ) {
436
+ right: 87px !important; /* Account for time container width + border */
437
+ }
438
+
439
+ /* Ensure proper clearfix for float layout */
440
+ .react-datepicker::after {
441
+ content: '' !important;
442
+ display: table !important;
443
+ clear: both !important;
444
+ }
445
+
446
+ /* Fix header layout to accommodate navigation buttons */
447
+ .react-datepicker__header {
448
+ text-align: center !important;
449
+ background-color: #f0f0f0 !important;
450
+ border-bottom: 1px solid #aeaeae !important;
451
+ border-top-left-radius: 0.3rem !important;
452
+ border-top-right-radius: 0.3rem !important;
453
+ padding: 8px 0 !important;
454
+ position: relative !important;
455
+ }
456
+
457
+ /* Ensure current month text doesn't interfere with navigation */
458
+ .react-datepicker__current-month,
459
+ .react-datepicker-time__header,
460
+ .react-datepicker-year-header {
461
+ margin-top: 0 !important;
462
+ color: #000 !important;
463
+ font-weight: bold !important;
464
+ font-size: 0.944rem !important;
465
+ padding: 0 40px !important; /* Add padding to avoid navigation button overlap */
387
466
  }
388
467
 
389
- /* Hide the arrow since we're centering the calendar */
468
+ /* Alternative approach: Force specific width for month container */
469
+ .react-datepicker__month-container {
470
+ width: calc(
471
+ 100% - 87px
472
+ ) !important; /* Full width minus time container and border */
473
+ box-sizing: border-box !important;
474
+ min-width: 295px !important; /* Ensure enough space for month/year dropdowns */
475
+ }
476
+
477
+ /* Ensure time container has proper sizing and positioning */
478
+ .react-datepicker__time-container {
479
+ height: auto !important;
480
+ min-height: 250px !important; /* Match typical calendar height */
481
+ }
482
+
483
+ /* Force container to maintain layout */
484
+ .react-datepicker {
485
+ overflow: hidden !important; /* Contain floated children */
486
+ }
487
+
488
+ /* Make month and year select dropdowns more compact */
489
+ .react-datepicker__month-dropdown-container,
490
+ .react-datepicker__year-dropdown-container {
491
+ display: inline-block !important;
492
+ margin: 0 5px !important;
493
+ }
494
+
495
+ /* Target the actual select elements with higher specificity */
496
+ .react-datepicker .react-datepicker__header .react-datepicker__month-select,
497
+ .react-datepicker .react-datepicker__header .react-datepicker__year-select {
498
+ width: auto !important;
499
+ min-width: 70px !important;
500
+ max-width: 90px !important;
501
+ padding: 4px 8px !important;
502
+ font-size: 0.9rem !important;
503
+ border: 1px solid #aeaeae !important;
504
+ border-radius: 4px !important;
505
+ background-color: white !important;
506
+ cursor: pointer !important;
507
+ text-align: center !important;
508
+ margin: 0 2px !important;
509
+ box-sizing: border-box !important;
510
+ }
511
+
512
+ .react-datepicker .react-datepicker__header .react-datepicker__year-select {
513
+ min-width: 60px !important;
514
+ max-width: 70px !important;
515
+ }
516
+
517
+ /* Also target read-view elements for dropdowns */
518
+ .react-datepicker__month-read-view,
519
+ .react-datepicker__year-read-view {
520
+ width: auto !important;
521
+ min-width: 70px !important;
522
+ max-width: 90px !important;
523
+ padding: 4px 8px !important;
524
+ font-size: 0.9rem !important;
525
+ border: 1px solid #aeaeae !important;
526
+ border-radius: 4px !important;
527
+ background-color: white !important;
528
+ cursor: pointer !important;
529
+ text-align: center !important;
530
+ }
531
+
532
+ .react-datepicker__year-read-view {
533
+ min-width: 60px !important;
534
+ max-width: 70px !important;
535
+ }
536
+
537
+ /* Ensure dropdowns fit in header */
538
+ .react-datepicker__header {
539
+ display: flex !important;
540
+ flex-direction: column !important;
541
+ align-items: center !important;
542
+ gap: 8px !important;
543
+ }
544
+
545
+ /* Container for month/year dropdowns */
546
+ .react-datepicker__current-month {
547
+ display: flex !important;
548
+ align-items: center !important;
549
+ justify-content: center !important;
550
+ gap: 8px !important;
551
+ padding: 0 40px !important;
552
+ }
553
+
554
+ /* Show arrow for proper positioning context */
390
555
  .react-datepicker__triangle {
391
- display: none !important;
556
+ display: block !important;
392
557
  }
393
558
 
394
- /* Ensure all containers allow the datepicker to be visible */
395
- .modal,
396
- .popup-content,
397
- .modal__content,
398
- .formItem,
399
- .fi__label,
400
- .dynamicform,
401
- .dynamic__left {
402
- overflow: visible !important;
559
+ /* Hide arrow only for center-screen mode */
560
+ .react-datepicker-popper.center-screen .react-datepicker__triangle {
561
+ display: none !important;
403
562
  }
404
563
 
405
- /* Add a backdrop to make the datepicker stand out */
406
- .react-datepicker-popper::before {
564
+ /* Backdrop only for center-screen mode */
565
+ .react-datepicker-popper.center-screen::before {
407
566
  content: '' !important;
408
567
  position: fixed !important;
409
568
  top: 0 !important;
@@ -466,8 +625,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
466
625
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
467
626
 
468
627
  /* Checkerboard pattern for transparency */
469
- background-image:
470
- linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
628
+ background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
471
629
  linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
472
630
  linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
473
631
  linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
@@ -478,8 +636,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
478
636
  .relation-color-preview:hover {
479
637
  border-color: #adb5bd;
480
638
  transform: scale(1.1);
481
- box-shadow:
482
- inset 0 1px 2px rgba(0, 0, 0, 0.1),
639
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1),
483
640
  0 2px 8px rgba(0, 0, 0, 0.15);
484
641
  }
485
642
 
@@ -535,8 +692,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
535
692
  flex-shrink: 0;
536
693
 
537
694
  /* Checkerboard pattern for transparency */
538
- background-image:
539
- linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
695
+ background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
540
696
  linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
541
697
  linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
542
698
  linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
@@ -545,18 +701,19 @@ form div:has(.react-toggle) input[type='checkbox'] {
545
701
  }
546
702
 
547
703
  /* When no color is selected */
548
- .cpicker__box[style=""] {
549
- background:
550
- linear-gradient(45deg,
551
- transparent 40%,
552
- #dc3545 42%,
553
- #dc3545 58%,
704
+ .cpicker__box[style=''] {
705
+ background: linear-gradient(
706
+ 45deg,
707
+ transparent 40%,
708
+ #dc3545 42%,
709
+ #dc3545 58%,
554
710
  transparent 60%
555
711
  ),
556
- linear-gradient(-45deg,
557
- transparent 40%,
558
- #dc3545 42%,
559
- #dc3545 58%,
712
+ linear-gradient(
713
+ -45deg,
714
+ transparent 40%,
715
+ #dc3545 42%,
716
+ #dc3545 58%,
560
717
  transparent 60%
561
718
  ),
562
719
  #ffffff;
@@ -600,7 +757,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
600
757
  .cpicker-container {
601
758
  padding: 6px;
602
759
  }
603
-
760
+
604
761
  .cpicker-container .w-color-sketch {
605
762
  transform: scale(0.75) !important;
606
763
  }
@@ -610,7 +767,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
610
767
  .cpicker-container {
611
768
  padding: 4px;
612
769
  }
613
-
770
+
614
771
  .cpicker-container .w-color-sketch {
615
772
  transform: scale(0.65) !important;
616
773
  }
@@ -0,0 +1,127 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ /**
5
+ * DatePickerPortal - Smart portal component for DatePicker positioning
6
+ *
7
+ * Automatically detects if the DatePicker is inside a modal context
8
+ * and renders it appropriately to prevent z-index conflicts
9
+ */
10
+ const DatePickerPortal = ({
11
+ children,
12
+ usePortal = null, // null = auto-detect, true = force portal, false = no portal
13
+ modalContext = null, // null = auto-detect, true = force modal styling, false = normal
14
+ centerScreen = false, // true = force center-screen positioning
15
+ containerRef = null // reference to the input container for positioning
16
+ }) => {
17
+ const [shouldUsePortal, setShouldUsePortal] = useState(false);
18
+ const [isModalContext, setIsModalContext] = useState(false);
19
+ const [portalContainer, setPortalContainer] = useState(null);
20
+
21
+ useEffect(() => {
22
+ // Auto-detect modal context if not explicitly set
23
+ if (modalContext === null) {
24
+ const detectModalContext = () => {
25
+ if (containerRef && containerRef.current) {
26
+ // Check if the container is inside a modal
27
+ const modalSelectors = [
28
+ '.modal',
29
+ '.popup-content',
30
+ '.modal__content',
31
+ '.swal2-container',
32
+ '[role="dialog"]',
33
+ '[aria-modal="true"]'
34
+ ];
35
+
36
+ for (const selector of modalSelectors) {
37
+ if (containerRef.current.closest(selector)) {
38
+ setIsModalContext(true);
39
+ setShouldUsePortal(true);
40
+ return;
41
+ }
42
+ }
43
+ }
44
+ setIsModalContext(false);
45
+ setShouldUsePortal(false);
46
+ };
47
+
48
+ detectModalContext();
49
+ } else {
50
+ setIsModalContext(modalContext);
51
+ setShouldUsePortal(modalContext);
52
+ }
53
+ }, [modalContext, containerRef]);
54
+
55
+ useEffect(() => {
56
+ // Override auto-detection if explicitly set
57
+ if (usePortal !== null) {
58
+ setShouldUsePortal(usePortal);
59
+ }
60
+ }, [usePortal]);
61
+
62
+ useEffect(() => {
63
+ // Create portal container
64
+ if (shouldUsePortal) {
65
+ const container = document.createElement('div');
66
+ container.className = 'datepicker-portal-container';
67
+
68
+ // Add appropriate classes based on context
69
+ if (isModalContext || centerScreen) {
70
+ container.classList.add('modal-context');
71
+ }
72
+
73
+ document.body.appendChild(container);
74
+ setPortalContainer(container);
75
+
76
+ // Add modal-context class to body for CSS targeting
77
+ if (isModalContext) {
78
+ document.body.classList.add('modal-with-datepicker');
79
+ }
80
+
81
+ return () => {
82
+ document.body.removeChild(container);
83
+ document.body.classList.remove('modal-with-datepicker');
84
+ };
85
+ }
86
+ }, [shouldUsePortal, isModalContext, centerScreen]);
87
+
88
+ // Clone children and add appropriate classes
89
+ const enhancedChildren = React.Children.map(children, child => {
90
+ if (React.isValidElement(child)) {
91
+ const additionalProps = {};
92
+
93
+ // Add popperClassName for react-datepicker
94
+ if (child.props.popperClassName) {
95
+ additionalProps.popperClassName = child.props.popperClassName;
96
+ }
97
+
98
+ // Add classes based on context
99
+ const classes = [];
100
+ if (isModalContext || centerScreen) {
101
+ classes.push('center-screen');
102
+ }
103
+ if (shouldUsePortal) {
104
+ classes.push('portal-mode');
105
+ }
106
+
107
+ if (classes.length > 0) {
108
+ additionalProps.popperClassName = [
109
+ child.props.popperClassName,
110
+ ...classes
111
+ ].filter(Boolean).join(' ');
112
+ }
113
+
114
+ return React.cloneElement(child, additionalProps);
115
+ }
116
+ return child;
117
+ });
118
+
119
+ // Render with or without portal
120
+ if (shouldUsePortal && portalContainer) {
121
+ return createPortal(enhancedChildren, portalContainer);
122
+ }
123
+
124
+ return enhancedChildren;
125
+ };
126
+
127
+ export default DatePickerPortal;
package/src/index.js CHANGED
@@ -29,6 +29,7 @@ import DataGridSearch from './components/controls/DataGridSearch';
29
29
  import AutoRefreshControls from './components/controls/AutoRefreshControls';
30
30
  import AudioPlayer from './components/controls/AudioPlayer';
31
31
  import GalleryModal from './components/modals/GalleryModal';
32
+ import DatePickerPortal from './components/utils/DatePickerPortal';
32
33
  export * from './components/columns/ColumnRenderers.jsx';
33
34
 
34
35
  /** Auth Specific Components */
@@ -96,6 +97,7 @@ export {
96
97
  CustomFetch,
97
98
  DataGrid,
98
99
  DataGridSearch,
100
+ DatePickerPortal,
99
101
  Download,
100
102
  DropZone,
101
103
  Field,