ui-soxo-bootstrap-core 2.6.29 → 2.6.30

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.
@@ -81,7 +81,7 @@
81
81
  }
82
82
 
83
83
  &.open {
84
- width: 256px;
84
+ width: var(--sidemenu-width, 211px);
85
85
  }
86
86
 
87
87
  &.close {
@@ -92,7 +92,7 @@
92
92
  transition: 0.3s;
93
93
 
94
94
  &.open {
95
- width: 256px;
95
+ width: var(--sidemenu-width, 211px);
96
96
  transition: 0.1s;
97
97
  }
98
98
 
@@ -109,7 +109,7 @@
109
109
  }
110
110
 
111
111
  @media only screen and (min-width: 800px) {
112
- width: 256px;
112
+ width: var(--sidemenu-width, 211px);
113
113
  }
114
114
  }
115
115
 
@@ -126,7 +126,7 @@
126
126
  overflow-x: scroll;
127
127
  position: relative;
128
128
  padding: 15px;
129
- margin-left: 212px;
129
+ margin-left: calc(var(--sidemenu-width, 211px) + 1px);
130
130
 
131
131
  &.kioskon {
132
132
  overflow-x: hidden;
@@ -213,9 +213,9 @@
213
213
  padding: 15px;
214
214
 
215
215
  &.open {
216
- width: calc(100% - 212px);
216
+ width: calc(100% - (var(--sidemenu-width, 211px) + 1px));
217
217
  transition: 0.1s;
218
- left: 212px;
218
+ left: calc(var(--sidemenu-width, 211px) + 1px);
219
219
  }
220
220
 
221
221
  @media only screen and (max-width: 768px) {
@@ -8,7 +8,7 @@
8
8
  *
9
9
  */
10
10
 
11
- import React, { useState, useEffect, useContext } from 'react';
11
+ import React, { useState, useEffect, useContext, useCallback, useRef } from 'react';
12
12
 
13
13
  import { animationControls, motion, useAnimation } from 'framer-motion';
14
14
 
@@ -24,10 +24,75 @@ import { useTranslation, Trans } from 'react-i18next';
24
24
 
25
25
  import { Menus } from '../../models';
26
26
 
27
+ import { expandFaAliases, ensureFontAwesomeAvailable } from '../../utils/font-awesome.utils';
28
+
27
29
  import './sidemenu.scss';
28
30
 
29
31
  const { SubMenu } = Menu;
30
32
 
33
+ const SIDEMENU_WIDTH_KEY = 'soxo:sidemenu-width';
34
+ const DEFAULT_SIDEMENU_WIDTH = 211;
35
+ const MIN_SIDEMENU_WIDTH = 180;
36
+ const MAX_SIDEMENU_WIDTH = 420;
37
+
38
+ /**
39
+ * Renders the menu's image if present, falling back to the Font Awesome icon.
40
+ * The FA icon is rendered immediately so something is always visible during
41
+ * a slow image fetch (e.g. cold reload, route change before the image is
42
+ * cached). The image takes over only after it successfully loads; if it
43
+ * fails, the FA icon stays.
44
+ */
45
+ function MenuIcon({ menu, icon, size = 25 }) {
46
+ const src = menu && typeof menu.image_path === 'string' ? menu.image_path.trim() : '';
47
+ const [imageStatus, setImageStatus] = useState(src ? 'loading' : 'none');
48
+ const imgRef = useRef(null);
49
+
50
+ // Reset and probe whenever src changes. This covers two cases that were
51
+ // breaking icon rendering on direct URL navigation / reload:
52
+ // 1. Cached images: the browser may fire `load` synchronously before
53
+ // React attaches the onLoad handler, leaving status stuck at
54
+ // 'loading'. We re-check `img.complete` after mount.
55
+ // 2. Broken responses (200 with empty/corrupt body, CSP blocked, etc.)
56
+ // that fire `load` but with naturalWidth=0 — treat as failed so the
57
+ // Font Awesome fallback shows instead of an invisible <img>.
58
+ useEffect(() => {
59
+ if (!src) {
60
+ setImageStatus('none');
61
+ return;
62
+ }
63
+ const img = imgRef.current;
64
+ if (img && img.complete) {
65
+ setImageStatus(img.naturalWidth > 0 ? 'loaded' : 'failed');
66
+ } else {
67
+ setImageStatus('loading');
68
+ }
69
+ }, [src]);
70
+
71
+ const handleLoad = () => {
72
+ const img = imgRef.current;
73
+ setImageStatus(img && img.naturalWidth > 0 ? 'loaded' : 'failed');
74
+ };
75
+
76
+ const showImage = src && imageStatus !== 'failed';
77
+ const showIcon = !showImage || imageStatus !== 'loaded';
78
+
79
+ return (
80
+ <>
81
+ {showIcon && <i className={`fa-solid fas ${expandFaAliases(icon)}`} />}
82
+ {showImage && (
83
+ <img
84
+ ref={imgRef}
85
+ style={{ width: size, display: imageStatus === 'loaded' ? 'inline-block' : 'none' }}
86
+ src={src}
87
+ alt=""
88
+ onLoad={handleLoad}
89
+ onError={() => setImageStatus('failed')}
90
+ />
91
+ )}
92
+ </>
93
+ );
94
+ }
95
+
31
96
  /**
32
97
  *
33
98
  * @param {*} collapsed
@@ -59,7 +124,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
59
124
  {!collapsed ? (
60
125
  <div className="menu-collapsed">
61
126
  <div>
62
- {menu && menu.image_path ? <img style={{ width: '25px' }} src={menu.image_path}></img> : <i className={`fa-solid fas ${icon}`} />}
127
+ <MenuIcon menu={menu} icon={icon} />
63
128
  </div>
64
129
 
65
130
  <div style={{ color: state.theme.colors.leftSectionColor }}>
@@ -73,7 +138,7 @@ function CollapsedIconMenu({ menu, collapsed, icon, caption }) {
73
138
  ) : (
74
139
  <div className="menu-collapsed">
75
140
  <span className="anticon">
76
- {menu && menu.image_path ? <img style={{ width: '25px' }} src={menu.image_path}></img> : <i className={`fa-solid fas ${icon}`} />}
141
+ <MenuIcon menu={menu} icon={icon} />
77
142
  </span>
78
143
 
79
144
  <span style={{ color: state.theme.colors.colorPrimaryText, paddingLeft: '6px' }}>
@@ -106,8 +171,62 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
106
171
  const [openKeys, setOpenKeys] = useState([]);
107
172
  const [menu, setMenu] = useState({});
108
173
 
174
+ const [sidebarWidth, setSidebarWidth] = useState(() => {
175
+ const saved = parseInt(typeof window !== 'undefined' ? localStorage.getItem(SIDEMENU_WIDTH_KEY) : null, 10);
176
+ return Number.isFinite(saved) && saved >= MIN_SIDEMENU_WIDTH && saved <= MAX_SIDEMENU_WIDTH ? saved : DEFAULT_SIDEMENU_WIDTH;
177
+ });
178
+ const [isDragging, setIsDragging] = useState(false);
179
+
109
180
  const { user = { locations: [] }, dispatch, state } = useContext(GlobalContext);
110
181
 
182
+ // Expose the current expanded sidebar width as a CSS variable so the
183
+ // surrounding layout (right-section, page-wrapper) can follow the drag.
184
+ useEffect(() => {
185
+ if (typeof document === 'undefined') return;
186
+ document.documentElement.style.setProperty('--sidemenu-width', `${sidebarWidth}px`);
187
+ }, [sidebarWidth]);
188
+
189
+ // Make sure Font Awesome is actually rendering glyphs in this app; if the
190
+ // host hasn't loaded it (or shipped a broken build), inject FA6 from a CDN
191
+ // so menu icons appear.
192
+ useEffect(() => {
193
+ ensureFontAwesomeAvailable();
194
+ }, []);
195
+
196
+ useEffect(() => {
197
+ if (typeof window === 'undefined') return;
198
+ localStorage.setItem(SIDEMENU_WIDTH_KEY, String(sidebarWidth));
199
+ }, [sidebarWidth]);
200
+
201
+ useEffect(() => {
202
+ if (!isDragging) return;
203
+
204
+ const handleMouseMove = (e) => {
205
+ const next = Math.min(Math.max(e.clientX, MIN_SIDEMENU_WIDTH), MAX_SIDEMENU_WIDTH);
206
+ setSidebarWidth(next);
207
+ };
208
+ const handleMouseUp = () => {
209
+ setIsDragging(false);
210
+ };
211
+
212
+ document.body.style.cursor = 'col-resize';
213
+ document.body.style.userSelect = 'none';
214
+ document.addEventListener('mousemove', handleMouseMove);
215
+ document.addEventListener('mouseup', handleMouseUp);
216
+
217
+ return () => {
218
+ document.body.style.cursor = '';
219
+ document.body.style.userSelect = '';
220
+ document.removeEventListener('mousemove', handleMouseMove);
221
+ document.removeEventListener('mouseup', handleMouseUp);
222
+ };
223
+ }, [isDragging]);
224
+
225
+ const handleResizeStart = useCallback((e) => {
226
+ e.preventDefault();
227
+ setIsDragging(true);
228
+ }, []);
229
+
111
230
  useEffect(() => {
112
231
  // Here we have to consider three cases now ,
113
232
  // One is firebase for which it is primarly designed for
@@ -528,6 +647,17 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
528
647
  {/* {renderFooter(footerLogo)} */}
529
648
  {/* Footer Logo Ends */}
530
649
  </div>
650
+
651
+ {!collapsed && (
652
+ <div
653
+ className={`sidemenu-resize-handle${isDragging ? ' dragging' : ''}`}
654
+ onMouseDown={handleResizeStart}
655
+ role="separator"
656
+ aria-orientation="vertical"
657
+ aria-label="Resize sidebar"
658
+ title="Drag to resize"
659
+ />
660
+ )}
531
661
  </div>
532
662
  );
533
663
  }
@@ -40,7 +40,7 @@
40
40
  padding: 5px 16px;
41
41
  position: fixed;
42
42
  z-index: 1000;
43
- width: 17%;
43
+ width: var(--sidemenu-width, 211px);
44
44
  background: #fff;
45
45
  // border-bottom: 1.5px solid #24aeb8;
46
46
  &.close {
@@ -106,7 +106,7 @@
106
106
  margin-top: 0;
107
107
  overflow-y: scroll;
108
108
  overflow-x: hidden;
109
- width: 211px;
109
+ width: var(--sidemenu-width, 211px);
110
110
  padding-bottom: 50px;
111
111
 
112
112
  /* Custom Scrollbar like macOS */
@@ -132,7 +132,7 @@
132
132
  }
133
133
  // when toggle open and close
134
134
  &.open {
135
- width: 211px !important;
135
+ width: var(--sidemenu-width, 211px) !important;
136
136
  }
137
137
 
138
138
  &.close {
@@ -202,6 +202,39 @@
202
202
  }
203
203
  }
204
204
 
205
+ .sidemenu-resize-handle {
206
+ position: fixed;
207
+ top: 0;
208
+ bottom: 0;
209
+ left: var(--sidemenu-width, 211px);
210
+ width: 6px;
211
+ margin-left: -3px;
212
+ cursor: col-resize;
213
+ z-index: 1100;
214
+ background: transparent;
215
+ transition: background 0.15s ease;
216
+
217
+ &::after {
218
+ content: '';
219
+ position: absolute;
220
+ top: 0;
221
+ bottom: 0;
222
+ left: 50%;
223
+ width: 1px;
224
+ background: transparent;
225
+ transition: background 0.15s ease;
226
+ }
227
+
228
+ &:hover::after,
229
+ &.dragging::after {
230
+ background: #24aeb8;
231
+ }
232
+
233
+ &.dragging {
234
+ background: rgba(36, 174, 184, 0.08);
235
+ }
236
+ }
237
+
205
238
  .sidebar-footer {
206
239
  position: fixed;
207
240
 
@@ -258,6 +291,10 @@
258
291
  .sidebar-footer {
259
292
  width: 100% !important;
260
293
  }
294
+
295
+ .sidemenu-resize-handle {
296
+ display: none;
297
+ }
261
298
  }
262
299
  }
263
300
 
@@ -1,10 +1,8 @@
1
- import React, { useState, useContext, useEffect } from 'react';
1
+ import { useContext, useEffect, useState } from 'react';
2
2
 
3
- import { Form, Input, message, Result, Radio, Divider, Typography } from 'antd';
3
+ import { Divider, Form, Input, message, Radio, Typography } from 'antd';
4
4
 
5
- import { withRouter, Link } from 'react-router-dom';
6
-
7
- import backgroundImage from './../../../assets/images/vector.png';
5
+ import { Link, withRouter } from 'react-router-dom';
8
6
 
9
7
  import OTPInput from 'otp-input-react';
10
8
 
@@ -32,7 +30,7 @@ import { Location } from '../../utils';
32
30
 
33
31
  import { checkLicenseStatus, formatMobile, safeJSON } from '../../utils/common/common.utils';
34
32
 
35
- import { MailOutlined, MessageOutlined, WhatsAppOutlined } from '@ant-design/icons';
33
+ import { MailOutlined, MessageOutlined } from '@ant-design/icons';
36
34
 
37
35
  import ResetPassword from './reset-password';
38
36
 
@@ -50,7 +48,6 @@ const tailLayout = {
50
48
 
51
49
  const LICENSE_EXPIRY = '2026-12-12';
52
50
 
53
-
54
51
  const headers = {
55
52
  db_ptr: 'nuraho',
56
53
  };
@@ -443,6 +440,20 @@ function LoginPhone({ history, appSettings }) {
443
440
  setOtpSuccess(false);
444
441
  };
445
442
 
443
+ // Paste handler runs in capture phase so it pre-empts otp-input-react's
444
+ // internal handler, which otherwise drops digits when the focused box is
445
+ // not the first one and does not strip spaces/dashes/surrounding text
446
+ const handleOtpPaste = (e) => {
447
+ const raw = e.clipboardData?.getData('text') || '';
448
+ const digits = raw.replace(/\D/g, '').slice(0, 6);
449
+ if (!digits) return;
450
+ e.preventDefault();
451
+ e.stopPropagation();
452
+ setOtpValue(digits);
453
+ setOtpError(false);
454
+ setOtpSuccess(false);
455
+ };
456
+
446
457
  /**
447
458
  * Otp resend Logic
448
459
  */
@@ -495,6 +506,18 @@ function LoginPhone({ history, appSettings }) {
495
506
  setOtpError(false);
496
507
  }
497
508
  }, [otpExpired]);
509
+
510
+ // Auto-submit once the user has filled all 6 digits (typed or pasted).
511
+ // verifyOtp clears otpValue on failure (back to length 0) and sets
512
+ // otpSuccess on success, both of which prevent this from re-firing.
513
+ useEffect(() => {
514
+ if (otpValue.length !== 6) return;
515
+ if (loading || otpExpired || otpSuccess || !otpVerification) return;
516
+ verifyOtp();
517
+ // verifyOtp intentionally omitted from deps — it's redefined every render;
518
+ // including it would re-fire on every state change.
519
+ // eslint-disable-next-line react-hooks/exhaustive-deps
520
+ }, [otpValue, otpVerification]);
498
521
  /**
499
522
  * Function to get Ldap users
500
523
  * @returns
@@ -611,28 +634,28 @@ function LoginPhone({ history, appSettings }) {
611
634
  return user.username;
612
635
  };
613
636
 
614
- const { globalCustomerHeader = () => { } } = appSettings;
637
+ const { globalCustomerHeader = () => {} } = appSettings;
615
638
 
616
639
  const themeName = process.env.REACT_APP_THEME; // e.g., 'purple'
617
640
  const isPurple = themeName === 'purple';
618
641
 
619
642
  const sectionStyle = isPurple
620
643
  ? {
621
- width: '100%',
622
- height: '100vh',
623
- backgroundImage: `${state.theme.colors.loginPageBackground}`,
624
- backgroundPosition: 'center bottom, center',
625
- backgroundRepeat: 'no-repeat, no-repeat',
626
- backgroundSize: 'cover, cover',
627
- }
644
+ width: '100%',
645
+ height: '100vh',
646
+ backgroundImage: `${state.theme.colors.loginPageBackground}`,
647
+ backgroundPosition: 'center bottom, center',
648
+ backgroundRepeat: 'no-repeat, no-repeat',
649
+ backgroundSize: 'cover, cover',
650
+ }
628
651
  : {
629
- width: '100%',
630
- height: '100vh',
631
- background: 'linear-gradient(to bottom, #F7F6E3 0%, #EEF1DE 20%, #D5E4DA 45%, #9DBFC8 75%, #4F89A6 100%)',
632
- backgroundPosition: 'center bottom, center',
633
- backgroundRepeat: 'no-repeat, no-repeat',
634
- backgroundSize: 'cover, cover',
635
- };
652
+ width: '100%',
653
+ height: '100vh',
654
+ background: 'linear-gradient(to bottom, #F7F6E3 0%, #EEF1DE 20%, #D5E4DA 45%, #9DBFC8 75%, #4F89A6 100%)',
655
+ backgroundPosition: 'center bottom, center',
656
+ backgroundRepeat: 'no-repeat, no-repeat',
657
+ backgroundSize: 'cover, cover',
658
+ };
636
659
 
637
660
  return (
638
661
  <section className="full-page" style={sectionStyle}>
@@ -732,26 +755,24 @@ function LoginPhone({ history, appSettings }) {
732
755
  <strong>{getOtpDestinationText()}</strong>
733
756
  </span>
734
757
  </p>
735
- <OTPInput
736
- value={otpValue}
737
- onChange={handleOtpChange}
738
- autoFocus
739
- OTPLength={6}
740
- disabled={loading || otpExpired}
741
- inputStyles={{
742
- border: otpSuccess
743
- ? '2px solid #52c41a' // green
744
- : otpError
745
- ? '2px solid #FF5C5C' // red
746
- : '1px solid #d9d9d9', // default
747
- borderRadius: 4,
748
- width: isMobile ? 28 : 45, // MOBILE FIX
749
- height: isMobile ? 36 : 40, // MOBILE FIX
750
- margin: isMobile ? '2px' : '0 4px',
751
- fontSize: isMobile ? 16 : 18,
752
- textAlign: 'center',
753
- }}
754
- />
758
+ <div onPasteCapture={handleOtpPaste}>
759
+ <OTPInput
760
+ value={otpValue}
761
+ onChange={handleOtpChange}
762
+ autoFocus
763
+ OTPLength={6}
764
+ disabled={loading || otpExpired}
765
+ inputStyles={{
766
+ border: otpSuccess ? '2px solid #52c41a' : otpError ? '2px solid #FF5C5C' : '1px solid #d9d9d9',
767
+ borderRadius: 4,
768
+ width: isMobile ? 28 : 45,
769
+ height: isMobile ? 36 : 40,
770
+ margin: isMobile ? '2px' : '0 4px',
771
+ fontSize: isMobile ? 16 : 18,
772
+ textAlign: 'center',
773
+ }}
774
+ />
775
+ </div>
755
776
  {/* Timer below OTP */}
756
777
  <div className="otp-timer">
757
778
  {/* {!otpExpired ? */}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Font Awesome compatibility helpers.
3
+ *
4
+ * Two failure modes are addressed here:
5
+ * 1. The host app ships FA6 Free but menu/data uses FA5 names that were
6
+ * renamed (e.g. fa-search-dollar → fa-magnifying-glass-dollar). Rendering
7
+ * the old class alone produces a blank glyph.
8
+ * 2. The host app doesn't ship Font Awesome at all, or ships a build whose
9
+ * font file fails to load. Every <i> renders blank regardless of class.
10
+ *
11
+ * `expandFaAliases` handles (1) by emitting both old and new class names so
12
+ * whichever the loaded stylesheet recognises will paint the glyph.
13
+ * `ensureFontAwesomeAvailable` handles (2) by probing the page for a working
14
+ * FA install and, if absent, injecting FA6 Free from a CDN.
15
+ */
16
+
17
+ // Font Awesome 5 → Font Awesome 6 Free renames. FA Pro keeps the FA5 names
18
+ // as aliases so emitting both is safe there too.
19
+ export const FA5_TO_FA6_FREE_ALIASES = {
20
+ // search / magnifier
21
+ 'fa-search': 'fa-magnifying-glass',
22
+ 'fa-search-dollar': 'fa-magnifying-glass-dollar',
23
+ 'fa-search-plus': 'fa-magnifying-glass-plus',
24
+ 'fa-search-minus': 'fa-magnifying-glass-minus',
25
+ 'fa-search-location': 'fa-magnifying-glass-location',
26
+ // layout / lists
27
+ 'fa-th': 'fa-table-cells',
28
+ 'fa-th-large': 'fa-table-cells-large',
29
+ 'fa-th-list': 'fa-table-list',
30
+ 'fa-list-alt': 'fa-rectangle-list',
31
+ 'fa-tasks': 'fa-list-check',
32
+ // auth / users
33
+ 'fa-sign-in-alt': 'fa-right-to-bracket',
34
+ 'fa-sign-out-alt': 'fa-right-from-bracket',
35
+ 'fa-user-circle': 'fa-circle-user',
36
+ 'fa-user-cog': 'fa-user-gear',
37
+ 'fa-user-md': 'fa-user-doctor',
38
+ 'fa-users-cog': 'fa-users-gear',
39
+ // common actions
40
+ 'fa-times': 'fa-xmark',
41
+ 'fa-times-circle': 'fa-circle-xmark',
42
+ 'fa-trash-alt': 'fa-trash-can',
43
+ 'fa-edit': 'fa-pen-to-square',
44
+ 'fa-pencil-alt': 'fa-pencil',
45
+ 'fa-save': 'fa-floppy-disk',
46
+ 'fa-share-alt': 'fa-share-nodes',
47
+ 'fa-unlink': 'fa-link-slash',
48
+ // settings / dashboards
49
+ 'fa-cog': 'fa-gear',
50
+ 'fa-cogs': 'fa-gears',
51
+ 'fa-tachometer-alt': 'fa-gauge',
52
+ 'fa-tachometer': 'fa-gauge',
53
+ // navigation / pages
54
+ 'fa-home': 'fa-house',
55
+ 'fa-file-alt': 'fa-file-lines',
56
+ // feedback / status
57
+ 'fa-info-circle': 'fa-circle-info',
58
+ 'fa-question-circle': 'fa-circle-question',
59
+ 'fa-check-circle': 'fa-circle-check',
60
+ 'fa-check-square': 'fa-square-check',
61
+ 'fa-exclamation-triangle': 'fa-triangle-exclamation',
62
+ 'fa-exclamation-circle': 'fa-circle-exclamation',
63
+ 'fa-plus-circle': 'fa-circle-plus',
64
+ 'fa-plus-square': 'fa-square-plus',
65
+ 'fa-minus-circle': 'fa-circle-minus',
66
+ 'fa-minus-square': 'fa-square-minus',
67
+ // charts
68
+ 'fa-chart-bar': 'fa-chart-column',
69
+ // calendar / time
70
+ 'fa-calendar-alt': 'fa-calendar-days',
71
+ 'fa-calendar-times': 'fa-calendar-xmark',
72
+ 'fa-history': 'fa-clock-rotate-left',
73
+ // money / billing
74
+ 'fa-money-check-alt': 'fa-money-check-dollar',
75
+ 'fa-money-bill-alt': 'fa-money-bill-1',
76
+ // shopping
77
+ 'fa-shopping-cart': 'fa-cart-shopping',
78
+ 'fa-shopping-bag': 'fa-bag-shopping',
79
+ 'fa-shopping-basket': 'fa-basket-shopping',
80
+ // devices / contact
81
+ 'fa-mobile-alt': 'fa-mobile-screen-button',
82
+ 'fa-tablet-alt': 'fa-tablet-screen-button',
83
+ 'fa-phone-alt': 'fa-phone',
84
+ 'fa-mail-bulk': 'fa-envelopes-bulk',
85
+ // map / location
86
+ 'fa-map-marker': 'fa-location-pin',
87
+ 'fa-map-marker-alt': 'fa-location-dot',
88
+ 'fa-globe-americas': 'fa-earth-americas',
89
+ // security / id
90
+ 'fa-shield-alt': 'fa-shield-halved',
91
+ 'fa-id-card-alt': 'fa-id-card-clip',
92
+ // healthcare (this codebase skews this way)
93
+ 'fa-prescription-bottle-alt': 'fa-prescription-bottle-medical',
94
+ 'fa-procedures': 'fa-bed-pulse',
95
+ 'fa-heartbeat': 'fa-heart-pulse',
96
+ 'fa-clinic-medical': 'fa-house-chimney-medical',
97
+ 'fa-hospital-alt': 'fa-hospital',
98
+ 'fa-allergies': 'fa-hand-dots',
99
+ 'fa-file-medical-alt': 'fa-file-waveform',
100
+ 'fa-running': 'fa-person-running',
101
+ 'fa-walking': 'fa-person-walking',
102
+ };
103
+
104
+ /**
105
+ * Expand a Font Awesome icon class string so that both the FA5 name and its
106
+ * FA6-Free equivalent are present. Same-name tokens are emitted once.
107
+ * Non-string input is returned as-is.
108
+ */
109
+ export function expandFaAliases(icon) {
110
+ if (typeof icon !== 'string' || !icon) return icon || '';
111
+ const seen = new Set();
112
+ const out = [];
113
+ for (const token of icon.split(/\s+/)) {
114
+ if (!token || seen.has(token)) continue;
115
+ seen.add(token);
116
+ out.push(token);
117
+ const alias = FA5_TO_FA6_FREE_ALIASES[token];
118
+ if (alias && !seen.has(alias)) {
119
+ seen.add(alias);
120
+ out.push(alias);
121
+ }
122
+ }
123
+ return out.join(' ');
124
+ }
125
+
126
+ const FA_FALLBACK_LINK_ID = 'soxo-fa-fallback';
127
+ const FA_FALLBACK_HREF = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css';
128
+
129
+ /**
130
+ * Probe the page for a working Font Awesome install. If the probe glyph
131
+ * doesn't render, inject FA6 Free from a CDN. No-op when FA already works
132
+ * or when the fallback has already been injected.
133
+ */
134
+ export function ensureFontAwesomeAvailable() {
135
+ if (typeof document === 'undefined' || !document.body) return;
136
+ if (document.getElementById(FA_FALLBACK_LINK_ID)) return;
137
+
138
+ // Render a hidden probe <i> with a class that exists in every recent FA
139
+ // build (`fa-circle`). If the ::before content is empty/none or the
140
+ // font-family doesn't claim to be Font Awesome, FA isn't usable here.
141
+ const probe = document.createElement('i');
142
+ probe.className = 'fa-solid fas fa-circle';
143
+ probe.style.cssText = 'position:absolute;left:-9999px;top:-9999px;visibility:hidden;';
144
+ document.body.appendChild(probe);
145
+
146
+ let working = false;
147
+ try {
148
+ const before = window.getComputedStyle(probe, '::before');
149
+ const content = before && before.content;
150
+ const fontFamily = before && before.fontFamily;
151
+ const hasGlyph = content && content !== 'none' && content !== '""' && content !== "''";
152
+ const isFaFont = fontFamily && /font\s*awesome/i.test(fontFamily);
153
+ working = Boolean(hasGlyph && isFaFont);
154
+ } catch (e) {
155
+ working = false;
156
+ } finally {
157
+ document.body.removeChild(probe);
158
+ }
159
+
160
+ if (working) return;
161
+
162
+ const link = document.createElement('link');
163
+ link.id = FA_FALLBACK_LINK_ID;
164
+ link.rel = 'stylesheet';
165
+ link.href = FA_FALLBACK_HREF;
166
+ link.crossOrigin = 'anonymous';
167
+ document.head.appendChild(link);
168
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.29",
3
+ "version": "2.6.30",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"