ui-soxo-bootstrap-core 2.6.1-dev.30 → 2.6.1-dev.31
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/core/modules/steps/steps.js +246 -4
- package/core/modules/steps/steps.scss +89 -0
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -1065,6 +1122,128 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1065
1122
|
};
|
|
1066
1123
|
}, [activeStep, steps, externalWin]);
|
|
1067
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* Touch-device detection.
|
|
1127
|
+
* - Runs once on mount.
|
|
1128
|
+
* - Uses `matchMedia('(pointer: coarse)')` as the primary signal because it
|
|
1129
|
+
* targets the actual input hardware (covers touch laptops correctly) and
|
|
1130
|
+
* falls back to `ontouchstart` / `navigator.maxTouchPoints` for older
|
|
1131
|
+
* browsers.
|
|
1132
|
+
* - When neither signal matches, the effect bails out and `isTouchDevice`
|
|
1133
|
+
* stays false so desktop renders without any touch-only UI.
|
|
1134
|
+
*/
|
|
1135
|
+
useEffect(() => {
|
|
1136
|
+
if (typeof window === 'undefined') {
|
|
1137
|
+
return undefined;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const hasCoarsePointer = typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
|
|
1141
|
+
const hasTouch = 'ontouchstart' in window || (typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0);
|
|
1142
|
+
|
|
1143
|
+
if (!hasCoarsePointer && !hasTouch) {
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
setIsTouchDevice(true);
|
|
1148
|
+
}, []);
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Show the floating prev/next arrow buttons and reset their auto-hide timer.
|
|
1152
|
+
* - Any pending hide timeout is cleared so a rapid sequence of touches keeps
|
|
1153
|
+
* the arrows on-screen continuously instead of flickering.
|
|
1154
|
+
* - A fresh timeout is scheduled for TOUCH_NAV_HIDE_DELAY so the arrows fade
|
|
1155
|
+
* away once the user stops interacting, keeping the step content clear.
|
|
1156
|
+
*/
|
|
1157
|
+
const revealTouchNav = () => {
|
|
1158
|
+
if (typeof window === 'undefined') {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
setTouchNavVisible(true);
|
|
1163
|
+
|
|
1164
|
+
if (touchNavHideTimeoutRef.current) {
|
|
1165
|
+
window.clearTimeout(touchNavHideTimeoutRef.current);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
touchNavHideTimeoutRef.current = window.setTimeout(() => {
|
|
1169
|
+
setTouchNavVisible(false);
|
|
1170
|
+
touchNavHideTimeoutRef.current = null;
|
|
1171
|
+
}, TOUCH_NAV_HIDE_DELAY);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* onTouchStart for the stage body.
|
|
1176
|
+
* - Records the initial touch position so handleStageTouchEnd can measure
|
|
1177
|
+
* the swipe delta.
|
|
1178
|
+
* - Also reveals the side arrows immediately, giving the user a visible
|
|
1179
|
+
* navigation affordance as soon as they touch the screen.
|
|
1180
|
+
*/
|
|
1181
|
+
const handleStageTouchStart = (event) => {
|
|
1182
|
+
if (!isTouchDevice || !event.touches || !event.touches.length) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const touch = event.touches[0];
|
|
1187
|
+
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
|
1188
|
+
revealTouchNav();
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* onTouchEnd for the stage body.
|
|
1193
|
+
* - Computes the horizontal/vertical delta against the stored touch origin.
|
|
1194
|
+
* - Ignores gestures that are vertical-dominant or below the distance
|
|
1195
|
+
* threshold, so normal scrolling and short taps are not hijacked.
|
|
1196
|
+
* - A left swipe advances to the next step (subject to the same
|
|
1197
|
+
* `isStepCompleted` / final-step rules as the visible Next button); a
|
|
1198
|
+
* right swipe goes back. Each successful swipe re-reveals the arrows so
|
|
1199
|
+
* the user can continue tapping if they prefer.
|
|
1200
|
+
*/
|
|
1201
|
+
const handleStageTouchEnd = (event) => {
|
|
1202
|
+
const start = touchStartRef.current;
|
|
1203
|
+
touchStartRef.current = null;
|
|
1204
|
+
|
|
1205
|
+
if (!start || !event.changedTouches || !event.changedTouches.length) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const touch = event.changedTouches[0];
|
|
1210
|
+
const deltaX = touch.clientX - start.x;
|
|
1211
|
+
const deltaY = touch.clientY - start.y;
|
|
1212
|
+
|
|
1213
|
+
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (Math.abs(deltaX) < SWIPE_DISTANCE_THRESHOLD) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (Math.abs(deltaY) > SWIPE_VERTICAL_TOLERANCE) {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (deltaX < 0) {
|
|
1224
|
+
const isFinalStep = steps[activeStep]?.order_seqtype === 'E';
|
|
1225
|
+
if (!isFinalStep && isStepCompleted && activeStep < steps.length - 1) {
|
|
1226
|
+
handleNext();
|
|
1227
|
+
revealTouchNav();
|
|
1228
|
+
}
|
|
1229
|
+
} else if (activeStep > 0) {
|
|
1230
|
+
handlePrevious();
|
|
1231
|
+
revealTouchNav();
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Cleanup any pending auto-hide timeout on unmount so the callback cannot
|
|
1237
|
+
* fire against a stale component and emit a React warning.
|
|
1238
|
+
*/
|
|
1239
|
+
useEffect(() => {
|
|
1240
|
+
return () => {
|
|
1241
|
+
if (typeof window !== 'undefined' && touchNavHideTimeoutRef.current) {
|
|
1242
|
+
window.clearTimeout(touchNavHideTimeoutRef.current);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
}, []);
|
|
1246
|
+
|
|
1068
1247
|
useEffect(() => {
|
|
1069
1248
|
if (typeof document === 'undefined') {
|
|
1070
1249
|
return undefined;
|
|
@@ -1180,12 +1359,12 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1180
1359
|
</div>
|
|
1181
1360
|
|
|
1182
1361
|
<div className="steps-nav-actions">
|
|
1183
|
-
<Button icon={isStepFullscreen ? <CompressOutlined /> : <ExpandOutlined />} onClick={toggleStepFullscreen}>
|
|
1362
|
+
<Button type="dashed" icon={isStepFullscreen ? <CompressOutlined /> : <ExpandOutlined />} onClick={toggleStepFullscreen}>
|
|
1184
1363
|
{isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
1185
1364
|
</Button>
|
|
1186
1365
|
|
|
1187
1366
|
{activeStep > 0 && (
|
|
1188
|
-
<Button icon={<ArrowLeftOutlined />} onClick={handlePrevious}>
|
|
1367
|
+
<Button type="default" icon={<ArrowLeftOutlined />} onClick={handlePrevious}>
|
|
1189
1368
|
Back
|
|
1190
1369
|
</Button>
|
|
1191
1370
|
)}
|
|
@@ -1219,14 +1398,77 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1219
1398
|
</>
|
|
1220
1399
|
) : (
|
|
1221
1400
|
<Button type="primary" disabled={!isStepCompleted} onClick={handleNext}>
|
|
1222
|
-
{
|
|
1401
|
+
{/*
|
|
1402
|
+
First-step label is resolved via FIRST_STEP_LABELS using
|
|
1403
|
+
the process name (lowercased + trimmed) as the key. Known
|
|
1404
|
+
processes get a tailored CTA (e.g. "Verify Profile",
|
|
1405
|
+
"Start Consultation"); unknown processes fall back to the
|
|
1406
|
+
generic "Next" label. All non-first steps always render
|
|
1407
|
+
"Next".
|
|
1408
|
+
*/}
|
|
1409
|
+
{activeStep === 0 ? (FIRST_STEP_LABELS[processName?.trim().toLowerCase()] ?? 'Next') : 'Next'}{' '}
|
|
1410
|
+
<ArrowRightOutlined />
|
|
1223
1411
|
</Button>
|
|
1224
1412
|
)}
|
|
1225
1413
|
</div>
|
|
1226
1414
|
</div>
|
|
1227
1415
|
|
|
1228
1416
|
<div className={`steps-content-panel${isStepFullscreen ? ' is-fullscreen' : ''}`}>
|
|
1229
|
-
|
|
1417
|
+
{/*
|
|
1418
|
+
Stage body:
|
|
1419
|
+
- `is-swipe-enabled` applies `touch-action: pan-y` so horizontal
|
|
1420
|
+
gestures reach our handlers while vertical scrolling remains
|
|
1421
|
+
native.
|
|
1422
|
+
- Touch handlers are only attached on touch devices to keep
|
|
1423
|
+
desktop event trees untouched.
|
|
1424
|
+
*/}
|
|
1425
|
+
<div
|
|
1426
|
+
className={`steps-stage-body${isTouchDevice ? ' is-swipe-enabled' : ''}`}
|
|
1427
|
+
onTouchStart={isTouchDevice ? handleStageTouchStart : undefined}
|
|
1428
|
+
onTouchEnd={isTouchDevice ? handleStageTouchEnd : undefined}
|
|
1429
|
+
>
|
|
1430
|
+
{/*
|
|
1431
|
+
Floating prev/next arrow buttons.
|
|
1432
|
+
- Rendered only on touch devices; `is-visible` class drives
|
|
1433
|
+
the fade-in/out via CSS transitions.
|
|
1434
|
+
- Disabled states mirror the visible Next/Back buttons in the
|
|
1435
|
+
top bar: previous disabled on the first step; next disabled
|
|
1436
|
+
on the last/final step or when the current step still
|
|
1437
|
+
requires user completion (isStepCompleted === false).
|
|
1438
|
+
- Clicking either button reveals the arrows again so the
|
|
1439
|
+
auto-hide timer restarts after every interaction.
|
|
1440
|
+
*/}
|
|
1441
|
+
{isTouchDevice ? (
|
|
1442
|
+
<>
|
|
1443
|
+
<button
|
|
1444
|
+
type="button"
|
|
1445
|
+
className={`steps-touch-nav steps-touch-nav-left${touchNavVisible ? ' is-visible' : ''}`}
|
|
1446
|
+
aria-label="Previous step"
|
|
1447
|
+
disabled={activeStep === 0}
|
|
1448
|
+
onClick={() => {
|
|
1449
|
+
revealTouchNav();
|
|
1450
|
+
if (activeStep > 0) handlePrevious();
|
|
1451
|
+
}}
|
|
1452
|
+
>
|
|
1453
|
+
<ArrowLeftOutlined />
|
|
1454
|
+
</button>
|
|
1455
|
+
<button
|
|
1456
|
+
type="button"
|
|
1457
|
+
className={`steps-touch-nav steps-touch-nav-right${touchNavVisible ? ' is-visible' : ''}`}
|
|
1458
|
+
aria-label="Next step"
|
|
1459
|
+
disabled={activeStep >= steps.length - 1 || steps[activeStep]?.order_seqtype === 'E' || !isStepCompleted}
|
|
1460
|
+
onClick={() => {
|
|
1461
|
+
revealTouchNav();
|
|
1462
|
+
const isFinalStep = steps[activeStep]?.order_seqtype === 'E';
|
|
1463
|
+
if (!isFinalStep && isStepCompleted && activeStep < steps.length - 1) {
|
|
1464
|
+
handleNext();
|
|
1465
|
+
}
|
|
1466
|
+
}}
|
|
1467
|
+
>
|
|
1468
|
+
<ArrowRightOutlined />
|
|
1469
|
+
</button>
|
|
1470
|
+
</>
|
|
1471
|
+
) : null}
|
|
1230
1472
|
<div
|
|
1231
1473
|
key={`${currentProcessId}_${activeStep}`}
|
|
1232
1474
|
className={`steps-chat-step-card ${stepSlideDirection === 'backward' ? 'slide-backward' : 'slide-forward'}`}
|
|
@@ -315,6 +315,81 @@
|
|
|
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: 48px;
|
|
332
|
+
height: 48px;
|
|
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.88);
|
|
340
|
+
color: #ffffff;
|
|
341
|
+
font-size: 18px;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.22);
|
|
344
|
+
opacity: 0;
|
|
345
|
+
visibility: hidden;
|
|
346
|
+
transform: translateY(-50%) scale(0.85);
|
|
347
|
+
transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 220ms, background-color 160ms ease;
|
|
348
|
+
-webkit-tap-highlight-color: transparent;
|
|
349
|
+
touch-action: manipulation;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.steps-touch-nav:hover:not(:disabled),
|
|
353
|
+
.steps-touch-nav:focus-visible:not(:disabled) {
|
|
354
|
+
background: rgba(30, 58, 138, 1);
|
|
355
|
+
outline: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.steps-touch-nav:active:not(:disabled) {
|
|
359
|
+
transform: translateY(-50%) scale(0.9);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.steps-touch-nav:disabled {
|
|
363
|
+
background: rgba(148, 163, 184, 0.7);
|
|
364
|
+
cursor: not-allowed;
|
|
365
|
+
box-shadow: 0 3px 10px rgba(15, 23, 42, 0.1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.steps-touch-nav-left {
|
|
369
|
+
left: 10px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.steps-touch-nav-right {
|
|
373
|
+
right: 10px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.steps-touch-nav.is-visible {
|
|
377
|
+
opacity: 1;
|
|
378
|
+
visibility: visible;
|
|
379
|
+
transform: translateY(-50%) scale(1);
|
|
380
|
+
transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 0s, background-color 160ms ease;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@media (prefers-reduced-motion: reduce) {
|
|
384
|
+
.steps-touch-nav {
|
|
385
|
+
transition: opacity 120ms linear, visibility 0s linear 120ms;
|
|
386
|
+
transform: translateY(-50%);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.steps-touch-nav.is-visible {
|
|
390
|
+
transform: translateY(-50%);
|
|
391
|
+
transition: opacity 120ms linear, visibility 0s linear 0s;
|
|
392
|
+
}
|
|
318
393
|
}
|
|
319
394
|
|
|
320
395
|
.steps-stage-body::-webkit-scrollbar {
|
|
@@ -665,6 +740,20 @@
|
|
|
665
740
|
.steps-narration-bar {
|
|
666
741
|
padding: 8px 12px;
|
|
667
742
|
}
|
|
743
|
+
|
|
744
|
+
.steps-touch-nav {
|
|
745
|
+
width: 42px;
|
|
746
|
+
height: 42px;
|
|
747
|
+
font-size: 16px;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.steps-touch-nav-left {
|
|
751
|
+
left: 6px;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.steps-touch-nav-right {
|
|
755
|
+
right: 6px;
|
|
756
|
+
}
|
|
668
757
|
}
|
|
669
758
|
|
|
670
759
|
/* ── Reduced motion ─────────────────────────────────────── */
|