Compare commits
2 Commits
cf019318a6
...
2a085e63a0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a085e63a0 | |||
| fa322ee0ce |
@@ -611,6 +611,7 @@ function createDeviceCard(device) {
|
|||||||
</a>
|
</a>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
<button class="card-tutorial-btn" onclick="startDeviceTutorial('${device.id}')" title="${t('device.tutorial.start')}">?</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -933,7 +934,12 @@ async function handleAddDevice(event) {
|
|||||||
console.log('Device added successfully:', result);
|
console.log('Device added successfully:', result);
|
||||||
showToast('Device added successfully', 'success');
|
showToast('Device added successfully', 'success');
|
||||||
closeAddDeviceModal();
|
closeAddDeviceModal();
|
||||||
loadDevices();
|
await loadDevices();
|
||||||
|
// Auto-start device tutorial on first device add
|
||||||
|
if (!localStorage.getItem('deviceTutorialSeen')) {
|
||||||
|
localStorage.setItem('deviceTutorialSeen', '1');
|
||||||
|
setTimeout(() => startDeviceTutorial(), 300);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
console.error('Failed to add device:', errorData);
|
console.error('Failed to add device:', errorData);
|
||||||
@@ -1090,7 +1096,14 @@ async function showCalibration(deviceId) {
|
|||||||
|
|
||||||
// Initialize span drag and render canvas after layout settles
|
// Initialize span drag and render canvas after layout settles
|
||||||
initSpanDrag();
|
initSpanDrag();
|
||||||
requestAnimationFrame(() => renderCalibrationCanvas());
|
requestAnimationFrame(() => {
|
||||||
|
renderCalibrationCanvas();
|
||||||
|
// Auto-start tutorial on first open
|
||||||
|
if (!localStorage.getItem('calibrationTutorialSeen')) {
|
||||||
|
localStorage.setItem('calibrationTutorialSeen', '1');
|
||||||
|
startCalibrationTutorial();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Re-render on container resize (e.g. window resize changes aspect-ratio container)
|
// Re-render on container resize (e.g. window resize changes aspect-ratio container)
|
||||||
if (!window._calibrationResizeObserver) {
|
if (!window._calibrationResizeObserver) {
|
||||||
@@ -1121,6 +1134,7 @@ function isCalibrationDirty() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function forceCloseCalibrationModal() {
|
function forceCloseCalibrationModal() {
|
||||||
|
closeTutorial();
|
||||||
const deviceId = document.getElementById('calibration-device-id').value;
|
const deviceId = document.getElementById('calibration-device-id').value;
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
clearTestMode(deviceId);
|
clearTestMode(deviceId);
|
||||||
@@ -1297,23 +1311,28 @@ function renderCalibrationCanvas() {
|
|||||||
const count = seg.led_count;
|
const count = seg.led_count;
|
||||||
if (count === 0) return;
|
if (count === 0) return;
|
||||||
|
|
||||||
// Mandatory ticks: first and last LED index per edge, plus LED 0 if offset > 0
|
// Edge boundary ticks (first/last LED on edge) and special ticks (LED 0 position)
|
||||||
const labelsToShow = new Set();
|
const edgeBounds = new Set();
|
||||||
labelsToShow.add(0);
|
edgeBounds.add(0);
|
||||||
if (count > 1) labelsToShow.add(count - 1);
|
if (count > 1) edgeBounds.add(count - 1);
|
||||||
|
|
||||||
|
const specialTicks = new Set();
|
||||||
if (offset > 0 && totalLeds > 0) {
|
if (offset > 0 && totalLeds > 0) {
|
||||||
const zeroPos = (totalLeds - seg.led_start % totalLeds) % totalLeds;
|
const zeroPos = (totalLeds - seg.led_start % totalLeds) % totalLeds;
|
||||||
if (zeroPos < count) labelsToShow.add(zeroPos);
|
if (zeroPos < count) specialTicks.add(zeroPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add intermediate ticks at "nice" intervals (max 5 labels per edge)
|
// Round-number ticks get priority; edge boundary labels suppressed if overlapping
|
||||||
|
const labelsToShow = new Set([...specialTicks]);
|
||||||
|
const tickLinesOnly = new Set();
|
||||||
|
|
||||||
if (count > 2) {
|
if (count > 2) {
|
||||||
const edgeLen = geo.horizontal ? (geo.x2 - geo.x1) : (geo.y2 - geo.y1);
|
const edgeLen = geo.horizontal ? (geo.x2 - geo.x1) : (geo.y2 - geo.y1);
|
||||||
const maxDigits = String(totalLeds > 0 ? totalLeds - 1 : count - 1).length;
|
const maxDigits = String(totalLeds > 0 ? totalLeds - 1 : count - 1).length;
|
||||||
const minSpacing = geo.horizontal ? maxDigits * 7 + 8 : 22;
|
const minSpacing = geo.horizontal ? maxDigits * 7 + 8 : 22;
|
||||||
|
|
||||||
const maxIntermediate = Math.max(0, 5 - labelsToShow.size);
|
const allMandatory = new Set([...edgeBounds, ...specialTicks]);
|
||||||
|
const maxIntermediate = Math.max(0, 5 - allMandatory.size);
|
||||||
const niceSteps = [5, 10, 25, 50, 100, 250, 500];
|
const niceSteps = [5, 10, 25, 50, 100, 250, 500];
|
||||||
let step = niceSteps[niceSteps.length - 1];
|
let step = niceSteps[niceSteps.length - 1];
|
||||||
for (const s of niceSteps) {
|
for (const s of niceSteps) {
|
||||||
@@ -1323,18 +1342,17 @@ function renderCalibrationCanvas() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pixel position helper (0..edgeLen along the edge)
|
|
||||||
const tickPx = i => {
|
const tickPx = i => {
|
||||||
const f = i / (count - 1);
|
const f = i / (count - 1);
|
||||||
return (seg.reverse ? (1 - f) : f) * edgeLen;
|
return (seg.reverse ? (1 - f) : f) * edgeLen;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collect pixel positions of mandatory ticks
|
// Phase 1: place round-number ticks (checked against specials + each other)
|
||||||
const placed = [];
|
const placed = [];
|
||||||
labelsToShow.forEach(i => placed.push(tickPx(i)));
|
specialTicks.forEach(i => placed.push(tickPx(i)));
|
||||||
|
|
||||||
// Add ticks at LED indices divisible by step
|
|
||||||
for (let i = 1; i < count - 1; i++) {
|
for (let i = 1; i < count - 1; i++) {
|
||||||
|
if (specialTicks.has(i)) continue;
|
||||||
const idx = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
|
const idx = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
|
||||||
if (idx % step === 0) {
|
if (idx % step === 0) {
|
||||||
const px = tickPx(i);
|
const px = tickPx(i);
|
||||||
@@ -1344,9 +1362,23 @@ function renderCalibrationCanvas() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: edge boundaries — show label unless overlapping a round-number tick
|
||||||
|
edgeBounds.forEach(bi => {
|
||||||
|
if (labelsToShow.has(bi) || specialTicks.has(bi)) return;
|
||||||
|
const px = tickPx(bi);
|
||||||
|
if (placed.some(p => Math.abs(px - p) < minSpacing)) {
|
||||||
|
tickLinesOnly.add(bi);
|
||||||
|
} else {
|
||||||
|
labelsToShow.add(bi);
|
||||||
|
placed.push(px);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
edgeBounds.forEach(i => labelsToShow.add(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tick styling — min/max ticks extend to container border, others short
|
// Tick styling
|
||||||
const tickLenLong = toggleSize + 3;
|
const tickLenLong = toggleSize + 3;
|
||||||
const tickLenShort = 4;
|
const tickLenShort = 4;
|
||||||
ctx.strokeStyle = tickStroke;
|
ctx.strokeStyle = tickStroke;
|
||||||
@@ -1354,45 +1386,66 @@ function renderCalibrationCanvas() {
|
|||||||
ctx.fillStyle = tickFill;
|
ctx.fillStyle = tickFill;
|
||||||
ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif';
|
ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif';
|
||||||
|
|
||||||
|
// Draw labeled ticks
|
||||||
labelsToShow.forEach(i => {
|
labelsToShow.forEach(i => {
|
||||||
const fraction = count > 1 ? i / (count - 1) : 0.5;
|
const fraction = count > 1 ? i / (count - 1) : 0.5;
|
||||||
const displayFraction = seg.reverse ? (1 - fraction) : fraction;
|
const displayFraction = seg.reverse ? (1 - fraction) : fraction;
|
||||||
const ledIndex = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
|
const ledIndex = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
|
||||||
const tickLen = (i === 0 || i === count - 1) ? tickLenLong : tickLenShort;
|
const tickLen = edgeBounds.has(i) ? tickLenLong : tickLenShort;
|
||||||
|
|
||||||
if (geo.horizontal) {
|
if (geo.horizontal) {
|
||||||
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
|
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
|
||||||
const axisY = axisPos[seg.edge];
|
const axisY = axisPos[seg.edge];
|
||||||
const tickDir = seg.edge === 'top' ? 1 : -1; // tick toward container
|
const tickDir = seg.edge === 'top' ? 1 : -1;
|
||||||
|
|
||||||
// Tick line
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(tx, axisY);
|
ctx.moveTo(tx, axisY);
|
||||||
ctx.lineTo(tx, axisY + tickDir * tickLen);
|
ctx.lineTo(tx, axisY + tickDir * tickLen);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Label outside
|
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = seg.edge === 'top' ? 'bottom' : 'top';
|
ctx.textBaseline = seg.edge === 'top' ? 'bottom' : 'top';
|
||||||
ctx.fillText(String(ledIndex), tx, axisY - tickDir * 1);
|
ctx.fillText(String(ledIndex), tx, axisY - tickDir * 1);
|
||||||
} else {
|
} else {
|
||||||
const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
|
const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
|
||||||
const axisX = axisPos[seg.edge];
|
const axisX = axisPos[seg.edge];
|
||||||
const tickDir = seg.edge === 'left' ? 1 : -1; // tick toward container
|
const tickDir = seg.edge === 'left' ? 1 : -1;
|
||||||
|
|
||||||
// Tick line
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(axisX, ty);
|
ctx.moveTo(axisX, ty);
|
||||||
ctx.lineTo(axisX + tickDir * tickLen, ty);
|
ctx.lineTo(axisX + tickDir * tickLen, ty);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Label outside
|
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.textAlign = seg.edge === 'left' ? 'right' : 'left';
|
ctx.textAlign = seg.edge === 'left' ? 'right' : 'left';
|
||||||
ctx.fillText(String(ledIndex), axisX - tickDir * 1, ty);
|
ctx.fillText(String(ledIndex), axisX - tickDir * 1, ty);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw tick lines only (no labels) for suppressed edge boundaries
|
||||||
|
tickLinesOnly.forEach(i => {
|
||||||
|
const fraction = count > 1 ? i / (count - 1) : 0.5;
|
||||||
|
const displayFraction = seg.reverse ? (1 - fraction) : fraction;
|
||||||
|
|
||||||
|
if (geo.horizontal) {
|
||||||
|
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
|
||||||
|
const axisY = axisPos[seg.edge];
|
||||||
|
const tickDir = seg.edge === 'top' ? 1 : -1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tx, axisY);
|
||||||
|
ctx.lineTo(tx, axisY + tickDir * tickLenLong);
|
||||||
|
ctx.stroke();
|
||||||
|
} else {
|
||||||
|
const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
|
||||||
|
const axisX = axisPos[seg.edge];
|
||||||
|
const tickDir = seg.edge === 'left' ? 1 : -1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(axisX, ty);
|
||||||
|
ctx.lineTo(axisX + tickDir * tickLenLong, ty);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Draw direction chevron at full-edge midpoint (not affected by span)
|
// Draw direction chevron at full-edge midpoint (not affected by span)
|
||||||
const s = 7;
|
const s = 7;
|
||||||
let mx, my, angle;
|
let mx, my, angle;
|
||||||
@@ -1789,6 +1842,7 @@ document.addEventListener('mousedown', (e) => {
|
|||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!e.target.classList.contains('modal')) return;
|
if (!e.target.classList.contains('modal')) return;
|
||||||
if (backdropMouseDownTarget !== e.target) return;
|
if (backdropMouseDownTarget !== e.target) return;
|
||||||
|
if (activeTutorial) return;
|
||||||
|
|
||||||
const modalId = e.target.id;
|
const modalId = e.target.id;
|
||||||
|
|
||||||
@@ -1832,3 +1886,229 @@ window.addEventListener('beforeunload', () => {
|
|||||||
clearInterval(refreshInterval);
|
clearInterval(refreshInterval);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tutorial System (generic engine)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
let activeTutorial = null;
|
||||||
|
// Shape: { steps, overlay, mode, step, resolveTarget, container }
|
||||||
|
// mode: 'absolute' (within a container) or 'fixed' (viewport-level)
|
||||||
|
|
||||||
|
const calibrationTutorialSteps = [
|
||||||
|
{ selector: '#cal-top-leds', textKey: 'calibration.tip.led_count', position: 'bottom' },
|
||||||
|
{ selector: '.corner-bottom-left', textKey: 'calibration.tip.start_corner', position: 'right' },
|
||||||
|
{ selector: '.direction-toggle', textKey: 'calibration.tip.direction', position: 'bottom' },
|
||||||
|
{ selector: '.offset-control', textKey: 'calibration.tip.offset', position: 'bottom' },
|
||||||
|
{ selector: '.edge-span-bar[data-edge="top"]', textKey: 'calibration.tip.span', position: 'bottom' },
|
||||||
|
{ selector: '.toggle-top', textKey: 'calibration.tip.test', position: 'top' },
|
||||||
|
{ selector: '.preview-screen-total', textKey: 'calibration.tip.toggle_inputs', position: 'bottom' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const deviceTutorialSteps = [
|
||||||
|
{ selector: '.card-subtitle', textKey: 'device.tip.metadata', position: 'bottom' },
|
||||||
|
{ selector: '.brightness-control', textKey: 'device.tip.brightness', position: 'bottom' },
|
||||||
|
{ selector: '.card-actions .btn:nth-child(1)', textKey: 'device.tip.start', position: 'top' },
|
||||||
|
{ selector: '.card-actions .btn:nth-child(2)', textKey: 'device.tip.settings', position: 'top' },
|
||||||
|
{ selector: '.card-actions .btn:nth-child(3)', textKey: 'device.tip.calibrate', position: 'top' },
|
||||||
|
{ selector: '.card-actions .btn:nth-child(4)', textKey: 'device.tip.webui', position: 'top' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function startTutorial(config) {
|
||||||
|
closeTutorial();
|
||||||
|
const overlay = document.getElementById(config.overlayId);
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
activeTutorial = {
|
||||||
|
steps: config.steps,
|
||||||
|
overlay: overlay,
|
||||||
|
mode: config.mode,
|
||||||
|
step: 0,
|
||||||
|
resolveTarget: config.resolveTarget,
|
||||||
|
container: config.container
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.classList.add('active');
|
||||||
|
document.addEventListener('keydown', handleTutorialKey);
|
||||||
|
showTutorialStep(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCalibrationTutorial() {
|
||||||
|
const container = document.querySelector('#calibration-modal .modal-body');
|
||||||
|
if (!container) return;
|
||||||
|
startTutorial({
|
||||||
|
steps: calibrationTutorialSteps,
|
||||||
|
overlayId: 'tutorial-overlay',
|
||||||
|
mode: 'absolute',
|
||||||
|
container: container,
|
||||||
|
resolveTarget: (step) => document.querySelector(step.selector)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDeviceTutorial(deviceId) {
|
||||||
|
// Resolve the device ID to target (don't capture card reference — it goes stale when loadDevices rebuilds DOM)
|
||||||
|
const selector = deviceId
|
||||||
|
? `.card[data-device-id="${deviceId}"]`
|
||||||
|
: '.card[data-device-id]';
|
||||||
|
if (!document.querySelector(selector)) return;
|
||||||
|
startTutorial({
|
||||||
|
steps: deviceTutorialSteps,
|
||||||
|
overlayId: 'device-tutorial-overlay',
|
||||||
|
mode: 'fixed',
|
||||||
|
container: null,
|
||||||
|
resolveTarget: (step) => {
|
||||||
|
const card = document.querySelector(selector);
|
||||||
|
if (!card) return null;
|
||||||
|
return step.global
|
||||||
|
? document.querySelector(step.selector)
|
||||||
|
: card.querySelector(step.selector);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTutorial() {
|
||||||
|
if (!activeTutorial) return;
|
||||||
|
activeTutorial.overlay.classList.remove('active');
|
||||||
|
document.querySelectorAll('.tutorial-target').forEach(el => {
|
||||||
|
el.classList.remove('tutorial-target');
|
||||||
|
el.style.zIndex = '';
|
||||||
|
});
|
||||||
|
document.removeEventListener('keydown', handleTutorialKey);
|
||||||
|
activeTutorial = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tutorialNext() {
|
||||||
|
if (!activeTutorial) return;
|
||||||
|
if (activeTutorial.step < activeTutorial.steps.length - 1) {
|
||||||
|
showTutorialStep(activeTutorial.step + 1);
|
||||||
|
} else {
|
||||||
|
closeTutorial();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tutorialPrev() {
|
||||||
|
if (!activeTutorial) return;
|
||||||
|
if (activeTutorial.step > 0) {
|
||||||
|
showTutorialStep(activeTutorial.step - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTutorialStep(index) {
|
||||||
|
if (!activeTutorial) return;
|
||||||
|
activeTutorial.step = index;
|
||||||
|
const step = activeTutorial.steps[index];
|
||||||
|
const overlay = activeTutorial.overlay;
|
||||||
|
const isFixed = activeTutorial.mode === 'fixed';
|
||||||
|
|
||||||
|
// Remove previous target highlight
|
||||||
|
document.querySelectorAll('.tutorial-target').forEach(el => {
|
||||||
|
el.classList.remove('tutorial-target');
|
||||||
|
el.style.zIndex = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find and highlight target
|
||||||
|
const target = activeTutorial.resolveTarget(step);
|
||||||
|
if (!target) return;
|
||||||
|
target.classList.add('tutorial-target');
|
||||||
|
// For fixed overlays, target must be above the z-index:10000 overlay
|
||||||
|
if (isFixed) target.style.zIndex = '10001';
|
||||||
|
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
const pad = 6;
|
||||||
|
let x, y, w, h;
|
||||||
|
|
||||||
|
if (isFixed) {
|
||||||
|
// Fixed mode: coordinates are viewport-relative
|
||||||
|
x = targetRect.left - pad;
|
||||||
|
y = targetRect.top - pad;
|
||||||
|
w = targetRect.width + pad * 2;
|
||||||
|
h = targetRect.height + pad * 2;
|
||||||
|
} else {
|
||||||
|
// Absolute mode: coordinates relative to container
|
||||||
|
const containerRect = activeTutorial.container.getBoundingClientRect();
|
||||||
|
x = targetRect.left - containerRect.left - pad;
|
||||||
|
y = targetRect.top - containerRect.top - pad;
|
||||||
|
w = targetRect.width + pad * 2;
|
||||||
|
h = targetRect.height + pad * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update backdrop clip-path (polygon with rectangular cutout)
|
||||||
|
const backdrop = overlay.querySelector('.tutorial-backdrop');
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.style.clipPath = `polygon(
|
||||||
|
0% 0%, 0% 100%,
|
||||||
|
${x}px 100%, ${x}px ${y}px,
|
||||||
|
${x + w}px ${y}px, ${x + w}px ${y + h}px,
|
||||||
|
${x}px ${y + h}px, ${x}px 100%,
|
||||||
|
100% 100%, 100% 0%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position ring around target
|
||||||
|
const ring = overlay.querySelector('.tutorial-ring');
|
||||||
|
if (ring) {
|
||||||
|
ring.style.left = x + 'px';
|
||||||
|
ring.style.top = y + 'px';
|
||||||
|
ring.style.width = w + 'px';
|
||||||
|
ring.style.height = h + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tooltip content
|
||||||
|
const tooltip = overlay.querySelector('.tutorial-tooltip');
|
||||||
|
const textEl = overlay.querySelector('.tutorial-tooltip-text');
|
||||||
|
const counterEl = overlay.querySelector('.tutorial-step-counter');
|
||||||
|
if (textEl) textEl.textContent = t(step.textKey);
|
||||||
|
if (counterEl) counterEl.textContent = `${index + 1} / ${activeTutorial.steps.length}`;
|
||||||
|
|
||||||
|
// Enable/disable nav buttons
|
||||||
|
const prevBtn = overlay.querySelector('.tutorial-prev-btn');
|
||||||
|
const nextBtn = overlay.querySelector('.tutorial-next-btn');
|
||||||
|
if (prevBtn) prevBtn.disabled = (index === 0);
|
||||||
|
if (nextBtn) nextBtn.textContent = (index === activeTutorial.steps.length - 1) ? '\u2713' : '\u2192';
|
||||||
|
|
||||||
|
// Position tooltip
|
||||||
|
if (tooltip) {
|
||||||
|
positionTutorialTooltip(tooltip, x, y, w, h, step.position, isFixed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionTutorialTooltip(tooltip, sx, sy, sw, sh, preferred, isFixed) {
|
||||||
|
const gap = 12;
|
||||||
|
const tooltipW = 260;
|
||||||
|
// Place offscreen to measure real height without visual flash
|
||||||
|
tooltip.setAttribute('style', 'left:-9999px;top:-9999px');
|
||||||
|
const tooltipH = tooltip.offsetHeight || 150;
|
||||||
|
|
||||||
|
const positions = {
|
||||||
|
top: { x: sx + sw / 2 - tooltipW / 2, y: sy - tooltipH - gap },
|
||||||
|
bottom: { x: sx + sw / 2 - tooltipW / 2, y: sy + sh + gap },
|
||||||
|
left: { x: sx - tooltipW - gap, y: sy + sh / 2 - tooltipH / 2 },
|
||||||
|
right: { x: sx + sw + gap, y: sy + sh / 2 - tooltipH / 2 }
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = positions[preferred] || positions.bottom;
|
||||||
|
|
||||||
|
const cW = isFixed ? window.innerWidth : activeTutorial.container.clientWidth;
|
||||||
|
const cH = isFixed ? window.innerHeight : activeTutorial.container.clientHeight;
|
||||||
|
|
||||||
|
// If preferred position overflows, try opposite
|
||||||
|
if (pos.y + tooltipH > cH || pos.y < 0 || pos.x + tooltipW > cW || pos.x < 0) {
|
||||||
|
const opposite = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
|
||||||
|
const alt = positions[opposite[preferred]];
|
||||||
|
if (alt && alt.y >= 0 && alt.y + tooltipH <= cH && alt.x >= 0 && alt.x + tooltipW <= cW) {
|
||||||
|
pos = alt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.x = Math.max(8, Math.min(cW - tooltipW - 8, pos.x));
|
||||||
|
pos.y = Math.max(8, Math.min(cH - tooltipH - 8, pos.y));
|
||||||
|
|
||||||
|
// Force-set all positioning via setAttribute to avoid any style-setting quirks
|
||||||
|
tooltip.setAttribute('style', `left:${Math.round(pos.x)}px;top:${Math.round(pos.y)}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTutorialKey(e) {
|
||||||
|
if (!activeTutorial) return;
|
||||||
|
if (e.key === 'Escape') { closeTutorial(); e.stopPropagation(); }
|
||||||
|
else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); tutorialNext(); }
|
||||||
|
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); tutorialPrev(); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,11 +80,11 @@
|
|||||||
<div class="modal-content" style="max-width: 700px;">
|
<div class="modal-content" style="max-width: 700px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 data-i18n="calibration.title">📐 LED Calibration</h2>
|
<h2 data-i18n="calibration.title">📐 LED Calibration</h2>
|
||||||
|
<button class="tutorial-trigger-btn" onclick="startCalibrationTutorial()" data-i18n-title="calibration.tutorial.start" title="Start tutorial">?</button>
|
||||||
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close">✕</button>
|
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="calibration-device-id">
|
<input type="hidden" id="calibration-device-id">
|
||||||
<p class="section-tip" data-i18n="calibration.preview.click_hint">Click an edge to toggle test LEDs on/off</p>
|
|
||||||
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
||||||
<div style="margin-bottom: 12px; padding: 0 24px;">
|
<div style="margin-bottom: 12px; padding: 0 24px;">
|
||||||
<div class="calibration-preview">
|
<div class="calibration-preview">
|
||||||
@@ -168,6 +168,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Tutorial Overlay -->
|
||||||
|
<div id="tutorial-overlay" class="tutorial-overlay">
|
||||||
|
<div class="tutorial-backdrop"></div>
|
||||||
|
<div class="tutorial-ring"></div>
|
||||||
|
<div class="tutorial-tooltip">
|
||||||
|
<div class="tutorial-tooltip-header">
|
||||||
|
<span class="tutorial-step-counter"></span>
|
||||||
|
<button class="tutorial-close-btn" onclick="closeTutorial()">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="tutorial-tooltip-text"></p>
|
||||||
|
<div class="tutorial-tooltip-nav">
|
||||||
|
<button class="tutorial-prev-btn" onclick="tutorialPrev()">←</button>
|
||||||
|
<button class="tutorial-next-btn" onclick="tutorialNext()">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="calibration-error" class="error-message" style="display: none;"></div>
|
<div id="calibration-error" class="error-message" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -304,6 +321,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Device Tutorial Overlay (viewport-level) -->
|
||||||
|
<div id="device-tutorial-overlay" class="tutorial-overlay tutorial-overlay-fixed">
|
||||||
|
<div class="tutorial-backdrop"></div>
|
||||||
|
<div class="tutorial-ring"></div>
|
||||||
|
<div class="tutorial-tooltip">
|
||||||
|
<div class="tutorial-tooltip-header">
|
||||||
|
<span class="tutorial-step-counter"></span>
|
||||||
|
<button class="tutorial-close-btn" onclick="closeTutorial()">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="tutorial-tooltip-text"></p>
|
||||||
|
<div class="tutorial-tooltip-nav">
|
||||||
|
<button class="tutorial-prev-btn" onclick="tutorialPrev()">←</button>
|
||||||
|
<button class="tutorial-next-btn" onclick="tutorialNext()">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Initialize theme
|
// Initialize theme
|
||||||
|
|||||||
@@ -77,6 +77,14 @@
|
|||||||
"device.health.online": "WLED Online",
|
"device.health.online": "WLED Online",
|
||||||
"device.health.offline": "WLED Offline",
|
"device.health.offline": "WLED Offline",
|
||||||
"device.health.checking": "Checking...",
|
"device.health.checking": "Checking...",
|
||||||
|
"device.tutorial.start": "Start tutorial",
|
||||||
|
"device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from WLED",
|
||||||
|
"device.tip.brightness": "Slide to adjust device brightness",
|
||||||
|
"device.tip.start": "Start or stop screen capture processing",
|
||||||
|
"device.tip.settings": "Configure device settings (display, FPS, health check)",
|
||||||
|
"device.tip.calibrate": "Calibrate LED positions, direction, and coverage",
|
||||||
|
"device.tip.webui": "Open WLED's built-in web interface for advanced configuration",
|
||||||
|
"device.tip.add": "Click here to add a new WLED device",
|
||||||
"settings.title": "Device Settings",
|
"settings.title": "Device Settings",
|
||||||
"settings.brightness": "Brightness:",
|
"settings.brightness": "Brightness:",
|
||||||
"settings.brightness.hint": "Global brightness for this WLED device (0-100%)",
|
"settings.brightness.hint": "Global brightness for this WLED device (0-100%)",
|
||||||
@@ -90,7 +98,14 @@
|
|||||||
"settings.saved": "Settings saved successfully",
|
"settings.saved": "Settings saved successfully",
|
||||||
"settings.failed": "Failed to save settings",
|
"settings.failed": "Failed to save settings",
|
||||||
"calibration.title": "LED Calibration",
|
"calibration.title": "LED Calibration",
|
||||||
"calibration.preview.click_hint": "Click an edge to toggle test LEDs on/off",
|
"calibration.tip.led_count": "Enter LED count per edge",
|
||||||
|
"calibration.tip.start_corner": "Click a corner to set the start position",
|
||||||
|
"calibration.tip.direction": "Toggle LED strip direction (clockwise / counterclockwise)",
|
||||||
|
"calibration.tip.offset": "Set LED offset — distance from LED 0 to the start corner",
|
||||||
|
"calibration.tip.span": "Drag green bars to adjust coverage span",
|
||||||
|
"calibration.tip.test": "Click an edge to toggle test LEDs",
|
||||||
|
"calibration.tip.toggle_inputs": "Click total LED count to toggle edge inputs",
|
||||||
|
"calibration.tutorial.start": "Start tutorial",
|
||||||
"calibration.start_position": "Starting Position:",
|
"calibration.start_position": "Starting Position:",
|
||||||
"calibration.position.bottom_left": "Bottom Left",
|
"calibration.position.bottom_left": "Bottom Left",
|
||||||
"calibration.position.bottom_right": "Bottom Right",
|
"calibration.position.bottom_right": "Bottom Right",
|
||||||
|
|||||||
@@ -77,6 +77,14 @@
|
|||||||
"device.health.online": "WLED Онлайн",
|
"device.health.online": "WLED Онлайн",
|
||||||
"device.health.offline": "WLED Недоступен",
|
"device.health.offline": "WLED Недоступен",
|
||||||
"device.health.checking": "Проверка...",
|
"device.health.checking": "Проверка...",
|
||||||
|
"device.tutorial.start": "Начать обучение",
|
||||||
|
"device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически из WLED",
|
||||||
|
"device.tip.brightness": "Перетащите для регулировки яркости",
|
||||||
|
"device.tip.start": "Запуск или остановка захвата экрана",
|
||||||
|
"device.tip.settings": "Настройки устройства (дисплей, FPS, интервал проверки)",
|
||||||
|
"device.tip.calibrate": "Калибровка позиций LED, направления и зоны покрытия",
|
||||||
|
"device.tip.webui": "Открыть встроенный веб-интерфейс WLED для расширенной настройки",
|
||||||
|
"device.tip.add": "Нажмите, чтобы добавить новое WLED устройство",
|
||||||
"settings.title": "Настройки Устройства",
|
"settings.title": "Настройки Устройства",
|
||||||
"settings.brightness": "Яркость:",
|
"settings.brightness": "Яркость:",
|
||||||
"settings.brightness.hint": "Общая яркость для этого WLED устройства (0-100%)",
|
"settings.brightness.hint": "Общая яркость для этого WLED устройства (0-100%)",
|
||||||
@@ -90,7 +98,14 @@
|
|||||||
"settings.saved": "Настройки успешно сохранены",
|
"settings.saved": "Настройки успешно сохранены",
|
||||||
"settings.failed": "Не удалось сохранить настройки",
|
"settings.failed": "Не удалось сохранить настройки",
|
||||||
"calibration.title": "Калибровка Светодиодов",
|
"calibration.title": "Калибровка Светодиодов",
|
||||||
"calibration.preview.click_hint": "Нажмите на край чтобы включить/выключить тест светодиодов",
|
"calibration.tip.led_count": "Укажите количество LED на каждой стороне",
|
||||||
|
"calibration.tip.start_corner": "Нажмите на угол для выбора стартовой позиции",
|
||||||
|
"calibration.tip.direction": "Переключение направления ленты (по часовой / против часовой)",
|
||||||
|
"calibration.tip.offset": "Смещение LED — расстояние от LED 0 до стартового угла",
|
||||||
|
"calibration.tip.span": "Перетащите зелёные полосы для настройки зоны покрытия",
|
||||||
|
"calibration.tip.test": "Нажмите на край для теста LED",
|
||||||
|
"calibration.tip.toggle_inputs": "Нажмите на общее количество LED для скрытия боковых полей",
|
||||||
|
"calibration.tutorial.start": "Начать обучение",
|
||||||
"calibration.start_position": "Начальная Позиция:",
|
"calibration.start_position": "Начальная Позиция:",
|
||||||
"calibration.position.bottom_left": "Нижний Левый",
|
"calibration.position.bottom_left": "Нижний Левый",
|
||||||
"calibration.position.bottom_right": "Нижний Правый",
|
"calibration.position.bottom_right": "Нижний Правый",
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ section {
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 12px 20px 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
@@ -215,6 +215,31 @@ section {
|
|||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-tutorial-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: 1.5px solid var(--border-color);
|
||||||
|
color: var(--text-muted, #777);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tutorial-btn:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
.card-remove-btn {
|
.card-remove-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@@ -242,7 +267,7 @@ section {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 10px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,6 +654,15 @@ section {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.section-tip {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.section-tip li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
@@ -1359,3 +1393,202 @@ input:-webkit-autofill:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tutorial System */
|
||||||
|
.tutorial-trigger-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-trigger-btn:hover {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#calibration-modal .modal-body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay.active {
|
||||||
|
display: block;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
transition: clip-path 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-ring {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: tutorial-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tutorial-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); }
|
||||||
|
50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
width: 260px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 102;
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: tutorial-tooltip-in 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tutorial-tooltip-in {
|
||||||
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-step-counter {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-close-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
background: rgba(128, 128, 128, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn,
|
||||||
|
.tutorial-next-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-next-btn {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn:hover:not(:disabled),
|
||||||
|
.tutorial-next-btn:hover:not(:disabled) {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn:disabled,
|
||||||
|
.tutorial-next-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-target {
|
||||||
|
position: relative;
|
||||||
|
z-index: 101 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed (viewport-level) tutorial overlay for device cards */
|
||||||
|
.tutorial-overlay-fixed {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay-fixed .tutorial-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay-fixed .tutorial-ring {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay-fixed .tutorial-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10002;
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* target z-index for fixed overlay is set inline via JS (target is outside overlay DOM) */
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user