@visns-studio/visns-components 5.11.3 → 5.11.4

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/README.md CHANGED
@@ -1650,6 +1650,61 @@ Modal components support vertical alignment configuration with the `verticalAlig
1650
1650
 
1651
1651
  The ReactDataGrid's `defaultGroupBy` prop enables internal grouping functionality that creates collapsible group headers within the table structure. It does NOT create any external UI elements that need relocation, as all grouping controls are internal to the table component.
1652
1652
 
1653
+ ### Navigation Component Icon Visibility Issues
1654
+
1655
+ #### Browser Rendering Optimization Fix
1656
+
1657
+ **Problem:** Navigation action icons (specifically anchor/Link elements) would disappear during page scroll in production environments. The issue only affected anchor tags while button elements remained visible. Notably, the problem disappeared when browser dev tools were open, indicating a browser rendering optimization conflict.
1658
+
1659
+ **Root Cause:** Firefox and other browsers apply aggressive rendering optimizations that can incorrectly cull elements from the paint tree. The combination of CSS `contain: layout` property with sticky positioning was causing anchor elements to be optimized out of view during scroll events.
1660
+
1661
+ **Solution Applied:**
1662
+
1663
+ 1. **Removed problematic containment properties:**
1664
+ - Removed `contain: layout` from `.content-header` and action elements
1665
+ - This property was causing layout optimization conflicts
1666
+
1667
+ 2. **Enhanced compositor layer creation:**
1668
+ - Changed `transform: translateZ(0)` to `transform: translateZ(0) translateX(0) translateY(0)`
1669
+ - Added `isolation: isolate` to force elements onto their own render layers
1670
+ - Added `backface-visibility: hidden` to prevent rendering glitches
1671
+
1672
+ 3. **Improved z-index stacking:**
1673
+ - Elevated z-index values from 100-103 range to 1000-1003 range
1674
+ - Ensures action icons stay above sticky header's stacking context
1675
+
1676
+ **Files Modified:**
1677
+ - `src/components/crm/styles/Navigation.module.scss` (content-header and action styling)
1678
+ - Enhanced both regular and alternate theme action containers
1679
+
1680
+ #### Navigation Component Debug Mode
1681
+
1682
+ The Navigation component now includes optional debugging capabilities to help diagnose rendering issues:
1683
+
1684
+ **Enable Debug Mode:**
1685
+ ```jsx
1686
+ <Navigation debugIcons={true} {...otherProps} />
1687
+ ```
1688
+
1689
+ **Debug Features:**
1690
+ - **Visual indicators:** Red borders around anchor elements with 'A' labels, blue borders around buttons with 'B' labels
1691
+ - **Console logging:** Detailed scroll event logs showing element positions, visibility, z-index, transforms, and containment properties
1692
+ - **Real-time monitoring:** Tracks icon visibility changes during scroll events
1693
+
1694
+ **Debug CSS Classes:**
1695
+ The component includes `.debug-icons` styles that can be applied for visual debugging:
1696
+ ```scss
1697
+ .debug-icons {
1698
+ a, button {
1699
+ border: 2px solid red !important;
1700
+ background: rgba(255, 0, 0, 0.1) !important;
1701
+ // Additional debug styling...
1702
+ }
1703
+ }
1704
+ ```
1705
+
1706
+ This fix ensures navigation icons remain visible during scroll across all browser environments and rendering contexts.
1707
+
1653
1708
  ## Support
1654
1709
 
1655
1710
  For support, please contact the VISNS Studio team or open an issue on GitHub.
package/package.json CHANGED
@@ -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.11.3",
90
+ "version": "5.11.4",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -18,6 +18,8 @@ import {
18
18
  Zap,
19
19
  ZapOff,
20
20
  Focus,
21
+ Bot,
22
+ Play,
21
23
  } from 'lucide-react';
22
24
 
23
25
  import CustomFetch from './Fetch';
@@ -50,10 +52,20 @@ const BusinessCardOcr = ({
50
52
  const [contactDecision, setContactDecision] = useState(null); // 'create', 'update', 'merge'
51
53
  const [isConsulting, setIsConsulting] = useState(false);
52
54
  const [isSaving, setIsSaving] = useState(false);
55
+
56
+ // Auto-capture states
57
+ const [autoCapture, setAutoCapture] = useState(true);
58
+ const [focusScore, setFocusScore] = useState(0);
59
+ const [isCountingDown, setIsCountingDown] = useState(false);
60
+ const [countdown, setCountdown] = useState(0);
61
+ const [focusThreshold] = useState(0.15); // Adjustable focus threshold
53
62
 
54
63
  const videoRef = useRef(null);
55
64
  const canvasRef = useRef(null);
56
65
  const fileInputRef = useRef(null);
66
+ const focusDetectionRef = useRef(null);
67
+ const countdownRef = useRef(null);
68
+ const capturePhotoRef = useRef(null);
57
69
 
58
70
  // Detect available cameras when component opens
59
71
  useEffect(() => {
@@ -203,6 +215,121 @@ const BusinessCardOcr = ({
203
215
  }
204
216
  };
205
217
 
218
+ // Calculate image sharpness/focus score using Laplacian variance
219
+ const calculateFocusScore = useCallback((videoElement) => {
220
+ if (!videoElement || videoElement.videoWidth === 0) return 0;
221
+
222
+ const canvas = document.createElement('canvas');
223
+ const ctx = canvas.getContext('2d');
224
+
225
+ // Use smaller canvas for performance
226
+ const width = 320;
227
+ const height = 240;
228
+ canvas.width = width;
229
+ canvas.height = height;
230
+
231
+ ctx.drawImage(videoElement, 0, 0, width, height);
232
+ const imageData = ctx.getImageData(0, 0, width, height);
233
+ const data = imageData.data;
234
+
235
+ // Convert to grayscale and apply Laplacian operator
236
+ let sum = 0;
237
+ let count = 0;
238
+
239
+ for (let y = 1; y < height - 1; y++) {
240
+ for (let x = 1; x < width - 1; x++) {
241
+ const i = (y * width + x) * 4;
242
+
243
+ // Convert to grayscale
244
+ const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
245
+
246
+ // Apply Laplacian operator (edge detection)
247
+ const laplacian = Math.abs(
248
+ -4 * gray +
249
+ data[((y - 1) * width + x) * 4] * 0.299 + data[((y - 1) * width + x) * 4 + 1] * 0.587 + data[((y - 1) * width + x) * 4 + 2] * 0.114 +
250
+ data[((y + 1) * width + x) * 4] * 0.299 + data[((y + 1) * width + x) * 4 + 1] * 0.587 + data[((y + 1) * width + x) * 4 + 2] * 0.114 +
251
+ data[(y * width + (x - 1)) * 4] * 0.299 + data[(y * width + (x - 1)) * 4 + 1] * 0.587 + data[(y * width + (x - 1)) * 4 + 2] * 0.114 +
252
+ data[(y * width + (x + 1)) * 4] * 0.299 + data[(y * width + (x + 1)) * 4 + 1] * 0.587 + data[(y * width + (x + 1)) * 4 + 2] * 0.114
253
+ );
254
+
255
+ sum += laplacian * laplacian;
256
+ count++;
257
+ }
258
+ }
259
+
260
+ // Return normalized variance (higher = sharper)
261
+ return count > 0 ? Math.sqrt(sum / count) / 255 : 0;
262
+ }, []);
263
+
264
+ // Auto-capture countdown logic
265
+ const startCountdown = useCallback(async () => {
266
+ if (isCountingDown || isFocusing) return;
267
+
268
+ setIsCountingDown(true);
269
+
270
+ for (let i = 3; i > 0; i--) {
271
+ setCountdown(i);
272
+ await new Promise(resolve => {
273
+ countdownRef.current = setTimeout(resolve, 1000);
274
+ });
275
+
276
+ // Check if still focused during countdown
277
+ if (videoRef.current) {
278
+ const currentFocus = calculateFocusScore(videoRef.current);
279
+ if (currentFocus < focusThreshold) {
280
+ setIsCountingDown(false);
281
+ setCountdown(0);
282
+ return; // Cancel if focus lost
283
+ }
284
+ }
285
+ }
286
+
287
+ setCountdown(0);
288
+ setIsCountingDown(false);
289
+
290
+ // Trigger capture using ref
291
+ if (capturePhotoRef.current) {
292
+ await capturePhotoRef.current();
293
+ }
294
+ }, [isCountingDown, isFocusing, calculateFocusScore, focusThreshold]);
295
+
296
+ // Focus detection loop
297
+ useEffect(() => {
298
+ if (!videoRef.current || !cameraReady || !autoCapture || currentStep !== 'camera') {
299
+ return;
300
+ }
301
+
302
+ const detectFocus = () => {
303
+ if (videoRef.current && !isCountingDown && !isFocusing) {
304
+ const score = calculateFocusScore(videoRef.current);
305
+ setFocusScore(score);
306
+
307
+ // Start countdown if image is in focus
308
+ if (score > focusThreshold && !isCountingDown) {
309
+ startCountdown();
310
+ }
311
+ }
312
+ };
313
+
314
+ // Check focus every 500ms
315
+ focusDetectionRef.current = setInterval(detectFocus, 500);
316
+
317
+ return () => {
318
+ if (focusDetectionRef.current) {
319
+ clearInterval(focusDetectionRef.current);
320
+ }
321
+ };
322
+ }, [cameraReady, autoCapture, currentStep, isCountingDown, isFocusing, calculateFocusScore, focusThreshold, startCountdown]);
323
+
324
+ // Cleanup countdown on unmount
325
+ useEffect(() => {
326
+ return () => {
327
+ if (countdownRef.current) {
328
+ clearTimeout(countdownRef.current);
329
+ }
330
+ };
331
+ }, []);
332
+
206
333
  // Image preprocessing pipeline
207
334
  const preprocessImage = async (imageBlob) => {
208
335
  return new Promise((resolve) => {
@@ -274,6 +401,11 @@ const BusinessCardOcr = ({
274
401
  );
275
402
  }, [cameraStream]);
276
403
 
404
+ // Assign capturePhoto to ref for use in startCountdown
405
+ useEffect(() => {
406
+ capturePhotoRef.current = capturePhoto;
407
+ }, [capturePhoto]);
408
+
277
409
  const switchCamera = () => {
278
410
  if (availableCameras.length > 1) {
279
411
  setCurrentCameraIndex(
@@ -1652,7 +1784,20 @@ const BusinessCardOcr = ({
1652
1784
  {currentStep === 'camera' && (
1653
1785
  <div className={ocrStyles.cameraContainer}>
1654
1786
  <div className={ocrStyles.captureGuide}>
1655
- <p>Position business card within the frame and tap to focus</p>
1787
+ <p>
1788
+ {autoCapture
1789
+ ? 'Position business card within frame - auto-capture when focused'
1790
+ : 'Position business card within the frame and tap to focus'
1791
+ }
1792
+ </p>
1793
+ <button
1794
+ onClick={() => setAutoCapture(!autoCapture)}
1795
+ className={`${ocrStyles.autoCaptureToggle} ${autoCapture ? ocrStyles.active : ''}`}
1796
+ title={autoCapture ? 'Disable auto-capture' : 'Enable auto-capture'}
1797
+ >
1798
+ {autoCapture ? <Bot size={16} /> : <Play size={16} />}
1799
+ {autoCapture ? 'Auto' : 'Manual'}
1800
+ </button>
1656
1801
  </div>
1657
1802
  <div className={ocrStyles.videoWrapper}>
1658
1803
  <video
@@ -1663,15 +1808,54 @@ const BusinessCardOcr = ({
1663
1808
  className={ocrStyles.cameraVideo}
1664
1809
  onClick={triggerAutofocus}
1665
1810
  />
1811
+ <canvas
1812
+ ref={canvasRef}
1813
+ style={{ display: 'none' }}
1814
+ />
1666
1815
  {/* Enhanced camera overlay with focus frame */}
1667
1816
  <div className={ocrStyles.cameraOverlay}>
1668
- <div className={ocrStyles.focusFrame}>
1817
+ <div className={`${ocrStyles.focusFrame} ${
1818
+ focusScore > focusThreshold ? ocrStyles.focused : ''
1819
+ } ${isCountingDown ? ocrStyles.countingDown : ''}`}>
1820
+
1821
+ {/* Focus corners */}
1822
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.topLeft}`}></div>
1823
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.topRight}`}></div>
1824
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.bottomLeft}`}></div>
1825
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.bottomRight}`}></div>
1826
+
1827
+ {/* Countdown display */}
1828
+ {isCountingDown && countdown > 0 && (
1829
+ <div className={ocrStyles.countdownDisplay}>
1830
+ <div className={ocrStyles.countdownNumber}>{countdown}</div>
1831
+ <div className={ocrStyles.countdownText}>Auto-capturing...</div>
1832
+ </div>
1833
+ )}
1834
+
1835
+ {/* Focus indicator */}
1669
1836
  {isFocusing && (
1670
1837
  <div className={ocrStyles.focusIndicator}>
1671
1838
  <Focus className={ocrStyles.focusIcon} />
1672
1839
  <span>Focusing...</span>
1673
1840
  </div>
1674
1841
  )}
1842
+
1843
+ {/* Focus status */}
1844
+ {autoCapture && !isFocusing && !isCountingDown && (
1845
+ <div className={ocrStyles.focusStatus}>
1846
+ <div className={`${ocrStyles.focusBar} ${
1847
+ focusScore > focusThreshold ? ocrStyles.good : ocrStyles.poor
1848
+ }`}>
1849
+ <div
1850
+ className={ocrStyles.focusBarFill}
1851
+ style={{ width: `${Math.min(focusScore * 300, 100)}%` }}
1852
+ ></div>
1853
+ </div>
1854
+ <span className={ocrStyles.focusText}>
1855
+ {focusScore > focusThreshold ? '✓ In Focus' : 'Move closer or adjust angle'}
1856
+ </span>
1857
+ </div>
1858
+ )}
1675
1859
  </div>
1676
1860
  </div>
1677
1861
  </div>
@@ -1704,8 +1888,9 @@ const BusinessCardOcr = ({
1704
1888
  fileInputRef.current?.click()
1705
1889
  }
1706
1890
  className={ocrStyles.secondaryButton}
1891
+ title="Upload image from device"
1707
1892
  >
1708
- <ImagePlus size={20} />
1893
+ <ImagePlus size={18} />
1709
1894
  Upload
1710
1895
  </button>
1711
1896
 
@@ -1713,9 +1898,10 @@ const BusinessCardOcr = ({
1713
1898
  onClick={capturePhoto}
1714
1899
  className={`${ocrStyles.captureButton} ${isFocusing ? ocrStyles.focusing : ''}`}
1715
1900
  disabled={isFocusing}
1901
+ title={isFocusing ? 'Focusing camera...' : 'Capture business card'}
1716
1902
  >
1717
1903
  <Camera size={28} />
1718
- {isFocusing ? 'Focusing...' : 'Capture'}
1904
+ {isFocusing ? 'Focus...' : 'Capture'}
1719
1905
  </button>
1720
1906
 
1721
1907
  <input
@@ -1725,8 +1911,6 @@ const BusinessCardOcr = ({
1725
1911
  accept="image/jpeg,image/png,image/jpg,image/gif,image/webp"
1726
1912
  style={{ display: 'none' }}
1727
1913
  />
1728
-
1729
- <div className={ocrStyles.placeholder}></div>
1730
1914
  </div>
1731
1915
  </div>
1732
1916
  </div>
@@ -183,6 +183,7 @@ function Navigation({
183
183
  setSystemAuth,
184
184
  themeType,
185
185
  userProfile,
186
+ debugIcons = false, // Add debug flag
186
187
  }) {
187
188
  const [search, setSearch] = useState('');
188
189
  const [searchResultShow, setSearchResultShow] = useState(false);
@@ -197,6 +198,7 @@ function Navigation({
197
198
  const [isScrolled, setIsScrolled] = useState(false);
198
199
  const [navData, setNavData] = useState(navConfig);
199
200
  const [showBusinessCardOcr, setShowBusinessCardOcr] = useState(false);
201
+ const actionItemsRef = useRef(null);
200
202
 
201
203
  const appNavClasses = styles['app-nav'];
202
204
 
@@ -294,7 +296,7 @@ function Navigation({
294
296
  to={n.url}
295
297
  data-tooltip-id="system-tooltip"
296
298
  data-tooltip-content={n.label}
297
- className={`tooltip ${activeClass}`}
299
+ className={`${activeClass}`}
298
300
  target={n.target ? n.target : '_self'}
299
301
  title={n.label}
300
302
  >
@@ -532,13 +534,65 @@ function Navigation({
532
534
  useEffect(() => {
533
535
  function handleScroll() {
534
536
  setIsScrolled(window.scrollY > 0);
537
+
538
+ // Debug logging for icon visibility
539
+ if (debugIcons && actionItemsRef.current) {
540
+ const anchors = actionItemsRef.current.querySelectorAll('a');
541
+ const buttons =
542
+ actionItemsRef.current.querySelectorAll('button');
543
+
544
+ console.log('=== SCROLL DEBUG ===');
545
+ console.log('Scroll Y:', window.scrollY);
546
+ console.log('Anchors found:', anchors.length);
547
+ console.log('Buttons found:', buttons.length);
548
+
549
+ anchors.forEach((anchor, index) => {
550
+ const rect = anchor.getBoundingClientRect();
551
+ const computedStyle = window.getComputedStyle(anchor);
552
+ console.log(`Anchor ${index}:`, {
553
+ visible: rect.width > 0 && rect.height > 0,
554
+ position: {
555
+ top: rect.top,
556
+ left: rect.left,
557
+ width: rect.width,
558
+ height: rect.height,
559
+ },
560
+ zIndex: computedStyle.zIndex,
561
+ display: computedStyle.display,
562
+ visibility: computedStyle.visibility,
563
+ opacity: computedStyle.opacity,
564
+ transform: computedStyle.transform,
565
+ contain: computedStyle.contain,
566
+ });
567
+ });
568
+
569
+ buttons.forEach((button, index) => {
570
+ const rect = button.getBoundingClientRect();
571
+ const computedStyle = window.getComputedStyle(button);
572
+ console.log(`Button ${index}:`, {
573
+ visible: rect.width > 0 && rect.height > 0,
574
+ position: {
575
+ top: rect.top,
576
+ left: rect.left,
577
+ width: rect.width,
578
+ height: rect.height,
579
+ },
580
+ zIndex: computedStyle.zIndex,
581
+ display: computedStyle.display,
582
+ visibility: computedStyle.visibility,
583
+ opacity: computedStyle.opacity,
584
+ transform: computedStyle.transform,
585
+ contain: computedStyle.contain,
586
+ });
587
+ });
588
+ }
535
589
  }
536
590
 
537
591
  window.addEventListener('scroll', handleScroll);
538
592
  return () => {
539
593
  window.removeEventListener('scroll', handleScroll);
540
594
  };
541
- }, []);
595
+ }, [debugIcons]);
542
596
 
543
597
  return layout === 'simple' || layout === 'cms' ? (
544
598
  <div className={styles.cwrap}>
@@ -601,7 +655,12 @@ function Navigation({
601
655
  <span></span>
602
656
  </button>
603
657
  </div>
604
- <div className={styles['hactions-alternate']}>
658
+ <div
659
+ className={`${styles['hactions-alternate']} ${
660
+ debugIcons ? styles['debug-icons'] : ''
661
+ }`}
662
+ ref={actionItemsRef}
663
+ >
605
664
  <ul>
606
665
  {navData.settings.map((setting, settingKey) =>
607
666
  setting.permission === true ? (
@@ -653,11 +712,12 @@ function Navigation({
653
712
  </ul>
654
713
  </div>
655
714
  <div
656
- className={
715
+ className={`${
657
716
  themeType === 'alternate'
658
717
  ? styles['hactions-alternate']
659
718
  : styles.hactions
660
- }
719
+ } ${debugIcons ? styles['debug-icons'] : ''}`}
720
+ ref={actionItemsRef}
661
721
  >
662
722
  <ul>
663
723
  {navData.settings.map((setting, settingKey) =>