ui-soxo-bootstrap-core 2.6.27 → 2.6.29

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.
@@ -21,7 +21,6 @@ import { Button } from '../../elements';
21
21
 
22
22
  import GenericHeader from '../header/generic-header';
23
23
 
24
- import * as AntIcons from '@ant-design/icons';
25
24
 
26
25
  import { CustomerServiceOutlined, MenuOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
27
26
 
@@ -263,16 +262,16 @@ function GlobalHeaderContent({ loading, appSettings, children, isConnected, hist
263
262
  {/* Help-desk-btn */}
264
263
  {helpDeskSetting?.showSupportBtn
265
264
  ? (() => {
266
- const HelpIcon = AntIcons[helpDeskSetting?.icon] ?? CustomerServiceOutlined;
267
265
  return (
268
266
  <Tooltip title={helpDeskSetting?.toolTipText} overlayClassName="modern-tooltip">
269
267
  <span>
270
268
  <Button
271
269
  onClick={() => window.open(helpDeskSetting?.helpDeskLink ?? '', '_blank')}
272
- icon={<HelpIcon />}
273
270
  type="default"
274
271
  size="small"
275
- />
272
+ >
273
+ {helpDeskSetting?.buttonText || 'Help'}
274
+ </Button>
276
275
  </span>
277
276
  </Tooltip>
278
277
  );
@@ -5,6 +5,8 @@
5
5
  * - Dynamically renders step-specific components based on configuration.
6
6
  * - Tracks step and process durations with local persistence support.
7
7
  * - Supports step navigation (next, previous, skip, breadcrumb, keyboard).
8
+ * - Touchscreen support: horizontal swipe gestures navigate between steps
9
+ * and transient left/right arrow buttons fade in on touch for discovery.
8
10
  * - Handles process submission and optional chaining to the next process.
9
11
  * - Renders a single active step view with compact breadcrumb controls.
10
12
  */
@@ -19,6 +21,21 @@ import { Button, Card, Location } from './../../lib';
19
21
  import { createOpenAIRealtimeSession, hasOpenAIRealtimeCredentials } from './openai-realtime';
20
22
  import './steps.scss';
21
23
 
24
+ const TOUCH_NAV_HIDE_DELAY = 2800;
25
+ const SWIPE_DISTANCE_THRESHOLD = 60;
26
+ const SWIPE_VERTICAL_TOLERANCE = 80;
27
+
28
+ /**
29
+ * First-step CTA labels keyed by normalized process name.
30
+ * - Keys are the lowercased/trimmed process name returned by the backend.
31
+ * - Missing keys fall back to the generic 'Next' label at the call site.
32
+ * - Frozen so accidental mutation during render doesn't leak across renders.
33
+ */
34
+ const FIRST_STEP_LABELS = Object.freeze({
35
+ verification: 'Verify Profile',
36
+ consultation: 'Start Consultation',
37
+ });
38
+
22
39
  const STEP_WELCOME_LINES = [
23
40
  'Welcome to your AI Automated Consultation process.',
24
41
  'You are in the right place for a smooth and guided health journey.',
@@ -273,6 +290,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
273
290
 
274
291
  const [loading, setLoading] = useState(false);
275
292
  const [steps, setSteps] = useState([]);
293
+ const [processName, setProcessName] = useState(null);
276
294
  const [activeStep, setActiveStep] = useState(0);
277
295
  const [isStepCompleted, setIsStepCompleted] = useState(false);
278
296
 
@@ -300,6 +318,8 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
300
318
  const [showNextProcessAction, setShowNextProcessAction] = useState(false);
301
319
  const [isStepFullscreen, setIsStepFullscreen] = useState(false);
302
320
  const [realtimeStatus, setRealtimeStatus] = useState('idle');
321
+ const [isTouchDevice, setIsTouchDevice] = useState(false);
322
+ const [touchNavVisible, setTouchNavVisible] = useState(false);
303
323
 
304
324
  const narrationUtteranceRef = useRef(null);
305
325
  const narrationAudioRef = useRef(null);
@@ -307,6 +327,8 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
307
327
  const narrationFallbackNoticeRef = useRef(false);
308
328
  const realtimeSessionRef = useRef(null);
309
329
  const fullscreenViewportRef = useRef(null);
330
+ const touchStartRef = useRef(null);
331
+ const touchNavHideTimeoutRef = useRef(null);
310
332
 
311
333
  const urlParams = Location.search();
312
334
  const isConsultationMode = String(urlParams?.consultation).toLowerCase() === 'true';
@@ -336,6 +358,40 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
336
358
  setShowNextProcessAction(false);
337
359
  }, [currentProcessId]);
338
360
 
361
+ /**
362
+ * Sync the loaded process name into the address bar.
363
+ * - Mirrors `processName` into a `process` query parameter so deep-links and
364
+ * refreshes carry the human-readable process label.
365
+ * - Uses `window.history.replaceState` to avoid a navigation event, which
366
+ * keeps React Router state and component instances stable.
367
+ * - Removes the param when `processName` is null/empty so stale values do
368
+ * not linger after a process clears.
369
+ */
370
+ useEffect(() => {
371
+ if (typeof window === 'undefined') {
372
+ return;
373
+ }
374
+
375
+ const params = new URLSearchParams(window.location.search);
376
+ const trimmedName = typeof processName === 'string' ? processName.trim() : '';
377
+
378
+ if (trimmedName) {
379
+ if (params.get('process') === trimmedName) {
380
+ return;
381
+ }
382
+ params.set('process', trimmedName);
383
+ } else {
384
+ if (!params.has('process')) {
385
+ return;
386
+ }
387
+ params.delete('process');
388
+ }
389
+
390
+ const search = params.toString();
391
+ const newUrl = `${window.location.pathname}${search ? `?${search}` : ''}${window.location.hash || ''}`;
392
+ window.history.replaceState(window.history.state, '', newUrl);
393
+ }, [processName]);
394
+
339
395
  //// Reset step start time whenever the active step changes
340
396
 
341
397
  useEffect(() => {
@@ -506,6 +562,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
506
562
  const result = await Dashboard.loadProcess(processId);
507
563
 
508
564
  setSteps(result?.data?.steps || []);
565
+ setProcessName(result?.data?.process_name ?? null);
509
566
  if (result?.data?.next_process_id) setNextProcessId(result.data);
510
567
  } catch (e) {
511
568
  console.error('Error loading process steps:', e);
@@ -1027,7 +1084,19 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
1027
1084
  * - Passes configuration, parameters, and handlers to the component.
1028
1085
  * - Handles missing steps or components gracefully.
1029
1086
  */
1030
- const DynamicComponent = () => {
1087
+ /**
1088
+ * Render the active step's dynamic component.
1089
+ *
1090
+ * Intentionally a plain function (not a component) called inline as
1091
+ * `{renderDynamicStep()}`. Defining it as a component inside the parent's
1092
+ * render body creates a fresh component *type* on every re-render — React
1093
+ * then unmounts and remounts the step component on every parent state
1094
+ * change (touchNavVisible, stepSlideDirection, activeStep timer, etc.),
1095
+ * which caused visible re-render/jitter during swipe navigation. Evaluating
1096
+ * it as a function just yields JSX for the real step Component, whose type
1097
+ * is stable, so React reconciles in place.
1098
+ */
1099
+ const renderDynamicStep = () => {
1031
1100
  const step = steps[activeStep];
1032
1101
  if (!step) return <Empty description="No step selected" />;
1033
1102
 
@@ -1065,6 +1134,128 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
1065
1134
  };
1066
1135
  }, [activeStep, steps, externalWin]);
1067
1136
 
1137
+ /**
1138
+ * Touch-device detection.
1139
+ * - Runs once on mount.
1140
+ * - Uses `matchMedia('(pointer: coarse)')` as the primary signal because it
1141
+ * targets the actual input hardware (covers touch laptops correctly) and
1142
+ * falls back to `ontouchstart` / `navigator.maxTouchPoints` for older
1143
+ * browsers.
1144
+ * - When neither signal matches, the effect bails out and `isTouchDevice`
1145
+ * stays false so desktop renders without any touch-only UI.
1146
+ */
1147
+ useEffect(() => {
1148
+ if (typeof window === 'undefined') {
1149
+ return undefined;
1150
+ }
1151
+
1152
+ const hasCoarsePointer = typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
1153
+ const hasTouch = 'ontouchstart' in window || (typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0);
1154
+
1155
+ if (!hasCoarsePointer && !hasTouch) {
1156
+ return undefined;
1157
+ }
1158
+
1159
+ setIsTouchDevice(true);
1160
+ }, []);
1161
+
1162
+ /**
1163
+ * Show the floating prev/next arrow buttons and reset their auto-hide timer.
1164
+ * - Any pending hide timeout is cleared so a rapid sequence of touches keeps
1165
+ * the arrows on-screen continuously instead of flickering.
1166
+ * - A fresh timeout is scheduled for TOUCH_NAV_HIDE_DELAY so the arrows fade
1167
+ * away once the user stops interacting, keeping the step content clear.
1168
+ */
1169
+ const revealTouchNav = () => {
1170
+ if (typeof window === 'undefined') {
1171
+ return;
1172
+ }
1173
+
1174
+ setTouchNavVisible(true);
1175
+
1176
+ if (touchNavHideTimeoutRef.current) {
1177
+ window.clearTimeout(touchNavHideTimeoutRef.current);
1178
+ }
1179
+
1180
+ touchNavHideTimeoutRef.current = window.setTimeout(() => {
1181
+ setTouchNavVisible(false);
1182
+ touchNavHideTimeoutRef.current = null;
1183
+ }, TOUCH_NAV_HIDE_DELAY);
1184
+ };
1185
+
1186
+ /**
1187
+ * onTouchStart for the stage body.
1188
+ * - Records the initial touch position so handleStageTouchEnd can measure
1189
+ * the swipe delta.
1190
+ * - Also reveals the side arrows immediately, giving the user a visible
1191
+ * navigation affordance as soon as they touch the screen.
1192
+ */
1193
+ const handleStageTouchStart = (event) => {
1194
+ if (!isTouchDevice || !event.touches || !event.touches.length) {
1195
+ return;
1196
+ }
1197
+
1198
+ const touch = event.touches[0];
1199
+ touchStartRef.current = { x: touch.clientX, y: touch.clientY };
1200
+ revealTouchNav();
1201
+ };
1202
+
1203
+ /**
1204
+ * onTouchEnd for the stage body.
1205
+ * - Computes the horizontal/vertical delta against the stored touch origin.
1206
+ * - Ignores gestures that are vertical-dominant or below the distance
1207
+ * threshold, so normal scrolling and short taps are not hijacked.
1208
+ * - A left swipe advances to the next step (subject to the same
1209
+ * `isStepCompleted` / final-step rules as the visible Next button); a
1210
+ * right swipe goes back. Each successful swipe re-reveals the arrows so
1211
+ * the user can continue tapping if they prefer.
1212
+ */
1213
+ const handleStageTouchEnd = (event) => {
1214
+ const start = touchStartRef.current;
1215
+ touchStartRef.current = null;
1216
+
1217
+ if (!start || !event.changedTouches || !event.changedTouches.length) {
1218
+ return;
1219
+ }
1220
+
1221
+ const touch = event.changedTouches[0];
1222
+ const deltaX = touch.clientX - start.x;
1223
+ const deltaY = touch.clientY - start.y;
1224
+
1225
+ if (Math.abs(deltaY) > Math.abs(deltaX)) {
1226
+ return;
1227
+ }
1228
+ if (Math.abs(deltaX) < SWIPE_DISTANCE_THRESHOLD) {
1229
+ return;
1230
+ }
1231
+ if (Math.abs(deltaY) > SWIPE_VERTICAL_TOLERANCE) {
1232
+ return;
1233
+ }
1234
+
1235
+ if (deltaX < 0) {
1236
+ const isFinalStep = steps[activeStep]?.order_seqtype === 'E';
1237
+ if (!isFinalStep && isStepCompleted && activeStep < steps.length - 1) {
1238
+ handleNext();
1239
+ revealTouchNav();
1240
+ }
1241
+ } else if (activeStep > 0) {
1242
+ handlePrevious();
1243
+ revealTouchNav();
1244
+ }
1245
+ };
1246
+
1247
+ /**
1248
+ * Cleanup any pending auto-hide timeout on unmount so the callback cannot
1249
+ * fire against a stale component and emit a React warning.
1250
+ */
1251
+ useEffect(() => {
1252
+ return () => {
1253
+ if (typeof window !== 'undefined' && touchNavHideTimeoutRef.current) {
1254
+ window.clearTimeout(touchNavHideTimeoutRef.current);
1255
+ }
1256
+ };
1257
+ }, []);
1258
+
1068
1259
  useEffect(() => {
1069
1260
  if (typeof document === 'undefined') {
1070
1261
  return undefined;
@@ -1180,12 +1371,12 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
1180
1371
  </div>
1181
1372
 
1182
1373
  <div className="steps-nav-actions">
1183
- <Button icon={isStepFullscreen ? <CompressOutlined /> : <ExpandOutlined />} onClick={toggleStepFullscreen}>
1374
+ <Button type="dashed" icon={isStepFullscreen ? <CompressOutlined /> : <ExpandOutlined />} onClick={toggleStepFullscreen}>
1184
1375
  {isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
1185
1376
  </Button>
1186
1377
 
1187
1378
  {activeStep > 0 && (
1188
- <Button icon={<ArrowLeftOutlined />} onClick={handlePrevious}>
1379
+ <Button type="default" icon={<ArrowLeftOutlined />} onClick={handlePrevious}>
1189
1380
  Back
1190
1381
  </Button>
1191
1382
  )}
@@ -1219,14 +1410,77 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
1219
1410
  </>
1220
1411
  ) : (
1221
1412
  <Button type="primary" disabled={!isStepCompleted} onClick={handleNext}>
1222
- {activeStep === 0 ? 'Start Consultation' : 'Next'} <ArrowRightOutlined />
1413
+ {/*
1414
+ First-step label is resolved via FIRST_STEP_LABELS using
1415
+ the process name (lowercased + trimmed) as the key. Known
1416
+ processes get a tailored CTA (e.g. "Verify Profile",
1417
+ "Start Consultation"); unknown processes fall back to the
1418
+ generic "Next" label. All non-first steps always render
1419
+ "Next".
1420
+ */}
1421
+ {activeStep === 0 ? (FIRST_STEP_LABELS[processName?.trim().toLowerCase()] ?? 'Next') : 'Next'}{' '}
1422
+ <ArrowRightOutlined />
1223
1423
  </Button>
1224
1424
  )}
1225
1425
  </div>
1226
1426
  </div>
1227
1427
 
1228
1428
  <div className={`steps-content-panel${isStepFullscreen ? ' is-fullscreen' : ''}`}>
1229
- <div className="steps-stage-body">
1429
+ {/*
1430
+ Stage body:
1431
+ - `is-swipe-enabled` applies `touch-action: pan-y` so horizontal
1432
+ gestures reach our handlers while vertical scrolling remains
1433
+ native.
1434
+ - Touch handlers are only attached on touch devices to keep
1435
+ desktop event trees untouched.
1436
+ */}
1437
+ <div
1438
+ className={`steps-stage-body${isTouchDevice ? ' is-swipe-enabled' : ''}`}
1439
+ onTouchStart={isTouchDevice ? handleStageTouchStart : undefined}
1440
+ onTouchEnd={isTouchDevice ? handleStageTouchEnd : undefined}
1441
+ >
1442
+ {/*
1443
+ Floating prev/next arrow buttons.
1444
+ - Rendered only on touch devices; `is-visible` class drives
1445
+ the fade-in/out via CSS transitions.
1446
+ - Disabled states mirror the visible Next/Back buttons in the
1447
+ top bar: previous disabled on the first step; next disabled
1448
+ on the last/final step or when the current step still
1449
+ requires user completion (isStepCompleted === false).
1450
+ - Clicking either button reveals the arrows again so the
1451
+ auto-hide timer restarts after every interaction.
1452
+ */}
1453
+ {isTouchDevice ? (
1454
+ <>
1455
+ <button
1456
+ type="button"
1457
+ className={`steps-touch-nav steps-touch-nav-left${touchNavVisible ? ' is-visible' : ''}`}
1458
+ aria-label="Previous step"
1459
+ disabled={activeStep === 0}
1460
+ onClick={() => {
1461
+ revealTouchNav();
1462
+ if (activeStep > 0) handlePrevious();
1463
+ }}
1464
+ >
1465
+ <ArrowLeftOutlined />
1466
+ </button>
1467
+ <button
1468
+ type="button"
1469
+ className={`steps-touch-nav steps-touch-nav-right${touchNavVisible ? ' is-visible' : ''}`}
1470
+ aria-label="Next step"
1471
+ disabled={activeStep >= steps.length - 1 || steps[activeStep]?.order_seqtype === 'E' || !isStepCompleted}
1472
+ onClick={() => {
1473
+ revealTouchNav();
1474
+ const isFinalStep = steps[activeStep]?.order_seqtype === 'E';
1475
+ if (!isFinalStep && isStepCompleted && activeStep < steps.length - 1) {
1476
+ handleNext();
1477
+ }
1478
+ }}
1479
+ >
1480
+ <ArrowRightOutlined />
1481
+ </button>
1482
+ </>
1483
+ ) : null}
1230
1484
  <div
1231
1485
  key={`${currentProcessId}_${activeStep}`}
1232
1486
  className={`steps-chat-step-card ${stepSlideDirection === 'backward' ? 'slide-backward' : 'slide-forward'}`}
@@ -1245,7 +1499,7 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
1245
1499
  <Spin />
1246
1500
  </div>
1247
1501
  ) : null}
1248
- {!loading ? <DynamicComponent /> : null}
1502
+ {!loading ? renderDynamicStep() : null}
1249
1503
  </div>
1250
1504
  </div>
1251
1505
  </div>
@@ -315,6 +315,177 @@
315
315
  overflow: hidden;
316
316
  padding: 10px 14px;
317
317
  box-sizing: border-box;
318
+ position: relative;
319
+ }
320
+
321
+ .steps-stage-body.is-swipe-enabled {
322
+ touch-action: pan-y;
323
+ }
324
+
325
+ /* ── Touch-device floating nav arrows ───────────────────── */
326
+
327
+ .steps-touch-nav {
328
+ position: absolute;
329
+ top: 50%;
330
+ z-index: 6;
331
+ width: 52px;
332
+ height: 52px;
333
+ display: inline-flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ padding: 0;
337
+ border: none;
338
+ border-radius: 50%;
339
+ background: rgba(30, 58, 138, 0.55);
340
+ backdrop-filter: blur(8px) saturate(140%);
341
+ -webkit-backdrop-filter: blur(8px) saturate(140%);
342
+ color: #ffffff;
343
+ font-size: 18px;
344
+ cursor: pointer;
345
+ box-shadow:
346
+ 0 10px 24px rgba(15, 23, 42, 0.22),
347
+ inset 0 0 0 1px rgba(255, 255, 255, 0.18);
348
+ opacity: 0;
349
+ visibility: hidden;
350
+ transform: translateY(-50%) scale(0.82);
351
+ transition:
352
+ opacity 260ms ease,
353
+ transform 260ms cubic-bezier(0.2, 0.8, 0.25, 1),
354
+ visibility 0s linear 260ms,
355
+ background-color 200ms ease,
356
+ box-shadow 200ms ease;
357
+ -webkit-tap-highlight-color: transparent;
358
+ touch-action: manipulation;
359
+ }
360
+
361
+ /*
362
+ Decorative pulsing ring around the button.
363
+ - Drawn via ::before so it does not interfere with click targets.
364
+ - Only animates while the button is visible and enabled, to keep the
365
+ idle state calm instead of visually noisy.
366
+ */
367
+ .steps-touch-nav::before {
368
+ content: '';
369
+ position: absolute;
370
+ inset: 0;
371
+ border-radius: 50%;
372
+ border: 2px solid rgba(255, 255, 255, 0.45);
373
+ opacity: 0;
374
+ pointer-events: none;
375
+ }
376
+
377
+ .steps-touch-nav.is-visible:not(:disabled)::before {
378
+ animation: steps-touch-nav-ring 2.4s ease-out infinite;
379
+ }
380
+
381
+ /*
382
+ Radial wash behind the icon for depth.
383
+ - Keeps the icon glyph crisp against varied content backgrounds.
384
+ */
385
+ .steps-touch-nav::after {
386
+ content: '';
387
+ position: absolute;
388
+ inset: 4px;
389
+ border-radius: 50%;
390
+ background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 65%);
391
+ pointer-events: none;
392
+ }
393
+
394
+ .steps-touch-nav .anticon {
395
+ position: relative;
396
+ z-index: 1;
397
+ filter: drop-shadow(0 1px 1px rgba(15, 23, 42, 0.25));
398
+ }
399
+
400
+ .steps-touch-nav:hover:not(:disabled),
401
+ .steps-touch-nav:focus-visible:not(:disabled) {
402
+ background: rgba(30, 58, 138, 0.92);
403
+ box-shadow:
404
+ 0 14px 30px rgba(15, 23, 42, 0.32),
405
+ inset 0 0 0 1px rgba(255, 255, 255, 0.28);
406
+ outline: none;
407
+ }
408
+
409
+ .steps-touch-nav:hover:not(:disabled),
410
+ .steps-touch-nav:focus-visible:not(:disabled),
411
+ .steps-touch-nav.is-visible:hover:not(:disabled),
412
+ .steps-touch-nav.is-visible:focus-visible:not(:disabled) {
413
+ opacity: 1;
414
+ }
415
+
416
+ .steps-touch-nav:active:not(:disabled) {
417
+ transform: translateY(-50%) scale(0.94);
418
+ transition:
419
+ transform 120ms ease,
420
+ opacity 120ms ease,
421
+ background-color 120ms ease,
422
+ box-shadow 120ms ease;
423
+ }
424
+
425
+ .steps-touch-nav:disabled {
426
+ background: rgba(148, 163, 184, 0.38);
427
+ color: rgba(255, 255, 255, 0.7);
428
+ cursor: not-allowed;
429
+ box-shadow: 0 3px 10px rgba(15, 23, 42, 0.08);
430
+ backdrop-filter: none;
431
+ -webkit-backdrop-filter: none;
432
+ }
433
+
434
+ .steps-touch-nav:disabled::after {
435
+ display: none;
436
+ }
437
+
438
+ .steps-touch-nav-left {
439
+ left: 14px;
440
+ }
441
+
442
+ .steps-touch-nav-right {
443
+ right: 14px;
444
+ }
445
+
446
+ .steps-touch-nav.is-visible {
447
+ opacity: 0.68;
448
+ visibility: visible;
449
+ transform: translateY(-50%) scale(1);
450
+ transition:
451
+ opacity 260ms ease,
452
+ transform 260ms cubic-bezier(0.2, 0.8, 0.25, 1),
453
+ visibility 0s linear 0s,
454
+ background-color 200ms ease,
455
+ box-shadow 200ms ease;
456
+ }
457
+
458
+ @keyframes steps-touch-nav-ring {
459
+ 0% {
460
+ opacity: 0.55;
461
+ transform: scale(1);
462
+ }
463
+
464
+ 65% {
465
+ opacity: 0;
466
+ transform: scale(1.45);
467
+ }
468
+
469
+ 100% {
470
+ opacity: 0;
471
+ transform: scale(1.45);
472
+ }
473
+ }
474
+
475
+ @media (prefers-reduced-motion: reduce) {
476
+ .steps-touch-nav {
477
+ transition: opacity 120ms linear, visibility 0s linear 120ms;
478
+ transform: translateY(-50%);
479
+ }
480
+
481
+ .steps-touch-nav.is-visible {
482
+ transform: translateY(-50%);
483
+ transition: opacity 120ms linear, visibility 0s linear 0s;
484
+ }
485
+
486
+ .steps-touch-nav.is-visible:not(:disabled)::before {
487
+ animation: none;
488
+ }
318
489
  }
319
490
 
320
491
  .steps-stage-body::-webkit-scrollbar {
@@ -665,6 +836,20 @@
665
836
  .steps-narration-bar {
666
837
  padding: 8px 12px;
667
838
  }
839
+
840
+ .steps-touch-nav {
841
+ width: 44px;
842
+ height: 44px;
843
+ font-size: 16px;
844
+ }
845
+
846
+ .steps-touch-nav-left {
847
+ left: 8px;
848
+ }
849
+
850
+ .steps-touch-nav-right {
851
+ right: 8px;
852
+ }
668
853
  }
669
854
 
670
855
  /* ── Reduced motion ─────────────────────────────────────── */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.27",
3
+ "version": "2.6.29",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"