Add HAOS scene preset buttons and smooth tutorial scrolling

Expose scene presets as button entities in the HA integration under a
dedicated "Scenes" device. Each button activates its scene via the API.
The coordinator now fetches scene presets alongside other data, and the
integration reloads when the scene list changes.

Also animate tutorial autoscroll with smooth behavior and wait for
scrollend before positioning the spotlight overlay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 19:53:47 +03:00
parent a34edf9650
commit 252db09145
7 changed files with 208 additions and 38 deletions

View File

@@ -226,35 +226,7 @@ export function tutorialPrev() {
}
}
function showTutorialStep(index, direction = 1) {
if (!activeTutorial) return;
activeTutorial.step = index;
const step = activeTutorial.steps[index];
const overlay = activeTutorial.overlay;
const isFixed = activeTutorial.mode === 'fixed';
document.querySelectorAll('.tutorial-target').forEach(el => {
el.classList.remove('tutorial-target');
el.style.zIndex = '';
});
const target = activeTutorial.resolveTarget(step);
if (!target) {
// Auto-skip hidden/missing targets in the current direction
const next = index + direction;
if (next >= 0 && next < activeTutorial.steps.length) showTutorialStep(next, direction);
else closeTutorial();
return;
}
target.classList.add('tutorial-target');
if (isFixed) target.style.zIndex = '10001';
// Scroll target into view if off-screen
const preRect = target.getBoundingClientRect();
if (preRect.bottom > window.innerHeight || preRect.top < 0) {
target.scrollIntoView({ behavior: 'instant', block: 'center' });
}
function _positionSpotlight(target, overlay, step, index, isFixed) {
const targetRect = target.getBoundingClientRect();
const pad = 6;
let x, y, w, h;
@@ -306,6 +278,58 @@ function showTutorialStep(index, direction = 1) {
}
}
/** Wait for smooth scroll to finish, then resolve. */
function _waitForScrollEnd(scrollTarget) {
return new Promise(resolve => {
let timer = setTimeout(resolve, 500); // fallback
const onEnd = () => {
clearTimeout(timer);
scrollTarget.removeEventListener('scrollend', onEnd);
resolve();
};
scrollTarget.addEventListener('scrollend', onEnd, { once: true });
});
}
function showTutorialStep(index, direction = 1) {
if (!activeTutorial) return;
activeTutorial.step = index;
const step = activeTutorial.steps[index];
const overlay = activeTutorial.overlay;
const isFixed = activeTutorial.mode === 'fixed';
document.querySelectorAll('.tutorial-target').forEach(el => {
el.classList.remove('tutorial-target');
el.style.zIndex = '';
});
const target = activeTutorial.resolveTarget(step);
if (!target) {
// Auto-skip hidden/missing targets in the current direction
const next = index + direction;
if (next >= 0 && next < activeTutorial.steps.length) showTutorialStep(next, direction);
else closeTutorial();
return;
}
target.classList.add('tutorial-target');
if (isFixed) target.style.zIndex = '10001';
// Scroll target into view if off-screen (smooth animation)
const preRect = target.getBoundingClientRect();
const needsScroll = preRect.bottom > window.innerHeight || preRect.top < 0;
if (needsScroll) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
_waitForScrollEnd(isFixed ? document.scrollingElement || document.documentElement : activeTutorial.container).then(() => {
if (activeTutorial && activeTutorial.step === index) {
_positionSpotlight(target, overlay, step, index, isFixed);
}
});
} else {
_positionSpotlight(target, overlay, step, index, isFixed);
}
}
function positionTutorialTooltip(tooltip, sx, sy, sw, sh, preferred, isFixed) {
const gap = 12;
const tooltipW = 260;