Drei zusammenhängende UI-Bausteine:
1) Tour-Engine ist jetzt page-agnostisch — sie liest die Stationen aus
``window.GWOE_TOUR_STEPS`` (pro Page hinterlegt), nicht mehr aus einem
eingebauten Konstanten. Tour-Komponente wird per ``{% include %}``
eingehängt; das Page-Template definiert vorher seine eigenen Steps.
Antrag-Detail-Tour wurde entsprechend in das eigene Template gezogen.
2) Startseite (v2/screens/durchsuchen.html): „Du bist neu hier?"-Banner
oben mit zwei Buttons — „🧭 Tour starten" und „Nein, danke". Banner
bleibt sichtbar, bis explizit weggeklickt wird (localStorage-Flag),
oder die Tour gestartet wird. Fünf Stationen für die Startseite:
Marken-Block, Suche, Score-Filter + Sortierung, Antrags-Liste,
linke Navigation.
3) Logo-Klick führt jetzt zur Startseite — sowohl in v2/base.html als
auch in components/appshell.html. ``v2-brand`` und ``v2-brand-sub``
sind in einen ``<a href="/">`` mit Hover-Highlight gewickelt
(``.v2-brand-link``).
Phase 2 (ElevenLabs-Voice) ist der nächste Schritt — bisher läuft das
Audio über die Web Speech API.
329 lines
11 KiB
HTML
329 lines
11 KiB
HTML
{# ─────────────────────────────────────────────────────────────────────
|
||
Geführte Tour-Engine (#185).
|
||
|
||
Was diese Komponente liefert:
|
||
- Spotlight-Overlay + Erklärblase + Vor/Zurück/Schließen + Mute
|
||
- Audio via Web Speech API (de-DE, möglichst weiblich)
|
||
- Engine: liest pro Page hinterlegte ``window.GWOE_TOUR_STEPS``
|
||
(Liste mit ``{selector, title, text}``), zeigt nur Stationen
|
||
deren Element wirklich da ist.
|
||
|
||
Pro Page:
|
||
<script>window.GWOE_TOUR_STEPS = [{selector: '...', title: '...', text: '...'}];</script>
|
||
{% include "v3/components/tour.html" %}
|
||
<button onclick="gwoeTourStart()">Tour starten</button>
|
||
|
||
Phase 1: Browser-TTS. Phase 2 (ElevenLabs) tauscht nur das
|
||
Audio-Backend; das Tour-Skript bleibt gleich.
|
||
───────────────────────────────────────────────────────────────────── #}
|
||
<div id="gwoe-tour-overlay" class="gwoe-tour-overlay" hidden aria-hidden="true">
|
||
<div id="gwoe-tour-spotlight" class="gwoe-tour-spotlight"></div>
|
||
<div id="gwoe-tour-bubble" class="gwoe-tour-bubble" role="dialog"
|
||
aria-labelledby="gwoe-tour-title" aria-describedby="gwoe-tour-text">
|
||
<div class="gwoe-tour-head">
|
||
<span id="gwoe-tour-step">Schritt 1 / 4</span>
|
||
<button type="button" onclick="gwoeTourEnd()" class="gwoe-tour-close"
|
||
title="Tour beenden" aria-label="Tour beenden">×</button>
|
||
</div>
|
||
<h3 id="gwoe-tour-title" class="gwoe-tour-title">Titel</h3>
|
||
<p id="gwoe-tour-text" class="gwoe-tour-text">Erklär-Text</p>
|
||
<div class="gwoe-tour-actions">
|
||
<button type="button" id="gwoe-tour-mute"
|
||
onclick="gwoeTourToggleMute()"
|
||
class="v3-action-btn gwoe-tour-mute"
|
||
title="Sprachausgabe an / aus">
|
||
<span id="gwoe-tour-mute-icon">🔊</span>
|
||
<span id="gwoe-tour-mute-label">Stimme an</span>
|
||
</button>
|
||
<span class="gwoe-tour-spacer"></span>
|
||
<button type="button" id="gwoe-tour-prev"
|
||
onclick="gwoeTourPrev()" class="v3-action-btn">← Zurück</button>
|
||
<button type="button" id="gwoe-tour-next"
|
||
onclick="gwoeTourNext()" class="v3-action-btn primary">Weiter →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.gwoe-tour-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 99998;
|
||
background: rgba(0, 0, 0, 0.55);
|
||
pointer-events: auto;
|
||
}
|
||
.gwoe-tour-overlay[hidden] { display: none; }
|
||
|
||
.gwoe-tour-spotlight {
|
||
position: absolute;
|
||
border-radius: 6px;
|
||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
|
||
pointer-events: none;
|
||
transition: top 0.3s ease, left 0.3s ease, width 0.3s ease, height 0.3s ease;
|
||
outline: 2px solid var(--ecg-teal);
|
||
outline-offset: 4px;
|
||
}
|
||
|
||
.gwoe-tour-bubble {
|
||
position: absolute;
|
||
z-index: 99999;
|
||
background: var(--ecg-card-bg, #fff);
|
||
color: var(--ecg-dark, #1a1a1a);
|
||
border: 1px solid var(--hairline, rgba(0,0,0,0.12));
|
||
border-radius: 6px;
|
||
padding: 14px 16px 12px;
|
||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
|
||
max-width: 380px;
|
||
font-family: var(--font-sans);
|
||
transition: top 0.3s ease, left 0.3s ease;
|
||
}
|
||
.gwoe-tour-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
opacity: 0.7;
|
||
margin-bottom: 4px;
|
||
}
|
||
.gwoe-tour-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 20px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
color: var(--ecg-dark);
|
||
opacity: 0.7;
|
||
padding: 0 4px;
|
||
}
|
||
.gwoe-tour-close:hover { opacity: 1; }
|
||
.gwoe-tour-title {
|
||
font-family: var(--font-display);
|
||
font-size: 16px;
|
||
margin: 4px 0 8px;
|
||
color: var(--ecg-teal);
|
||
}
|
||
.gwoe-tour-text {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
margin: 0 0 12px;
|
||
}
|
||
.gwoe-tour-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
border-top: 1px dashed var(--hairline, rgba(0,0,0,0.1));
|
||
padding-top: 10px;
|
||
}
|
||
.gwoe-tour-spacer { flex: 1 1 auto; }
|
||
.gwoe-tour-mute { font-size: 12px; }
|
||
.gwoe-tour-bubble .v3-action-btn.primary {
|
||
background: var(--ecg-teal);
|
||
color: #fff;
|
||
border-color: var(--ecg-teal);
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.gwoe-tour-bubble {
|
||
/* auf Mobile zentriert unten andocken statt gefloated, sonst kollidiert
|
||
mit dem Spotlight */
|
||
left: 12px !important;
|
||
right: 12px !important;
|
||
max-width: none;
|
||
bottom: 12px;
|
||
top: auto !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
(function () {
|
||
// Tour-Stationen kommen pro Page via window.GWOE_TOUR_STEPS.
|
||
// Jeder Eintrag: {selector, title, text}. Selectoren werden zur Laufzeit
|
||
// aufgelöst — fehlt das Element (z.B. keine Plenum-Votes auf einer
|
||
// bestimmten Drucksache), überspringt die Tour den Schritt automatisch.
|
||
function getSteps() {
|
||
return Array.isArray(window.GWOE_TOUR_STEPS) ? window.GWOE_TOUR_STEPS : [];
|
||
}
|
||
|
||
let _tourIdx = 0;
|
||
let _resolvedSteps = []; // STEPS gefiltert auf vorhandene Elemente
|
||
let _tourMuted = false;
|
||
let _tourUtter = null;
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
function speak(text) {
|
||
if (_tourMuted) return;
|
||
if (!('speechSynthesis' in window)) return;
|
||
try {
|
||
window.speechSynthesis.cancel();
|
||
const u = new SpeechSynthesisUtterance(text);
|
||
u.lang = 'de-DE';
|
||
u.rate = 1.0;
|
||
u.pitch = 1.0;
|
||
// Versuche eine deutsche, möglichst weibliche Stimme zu wählen.
|
||
const voices = window.speechSynthesis.getVoices();
|
||
const de = voices.filter(v => /de(-DE|_DE)?/i.test(v.lang));
|
||
const female = de.find(v => /female|frau|anna|petra|katja|helga|marlene|vicki/i.test(v.name));
|
||
u.voice = female || de[0] || null;
|
||
_tourUtter = u;
|
||
window.speechSynthesis.speak(u);
|
||
} catch (_) { /* TTS optional */ }
|
||
}
|
||
|
||
function stopSpeak() {
|
||
if ('speechSynthesis' in window) {
|
||
try { window.speechSynthesis.cancel(); } catch (_) {}
|
||
}
|
||
_tourUtter = null;
|
||
}
|
||
|
||
function resolveSteps() {
|
||
_resolvedSteps = [];
|
||
for (const s of getSteps()) {
|
||
// Erstes Match aus comma-separated Selector-Liste.
|
||
const parts = s.selector.split(',').map(x => x.trim());
|
||
let el = null;
|
||
for (const p of parts) {
|
||
el = document.querySelector(p);
|
||
if (el) break;
|
||
}
|
||
if (el) _resolvedSteps.push({ ...s, el });
|
||
}
|
||
}
|
||
|
||
function positionBubble(el) {
|
||
const rect = el.getBoundingClientRect();
|
||
const bubble = $('gwoe-tour-bubble');
|
||
const spotlight = $('gwoe-tour-spotlight');
|
||
|
||
// Spotlight um das Element legen.
|
||
spotlight.style.top = (rect.top - 4) + 'px';
|
||
spotlight.style.left = (rect.left - 4) + 'px';
|
||
spotlight.style.width = (rect.width + 8) + 'px';
|
||
spotlight.style.height = (rect.height + 8) + 'px';
|
||
|
||
// Bubble unter oder über das Element legen, wo Platz ist.
|
||
const bubbleH = 240; // grobe Annahme
|
||
const spaceBelow = window.innerHeight - rect.bottom;
|
||
const spaceAbove = rect.top;
|
||
let top, left;
|
||
if (spaceBelow > bubbleH + 20) {
|
||
top = rect.bottom + 14;
|
||
} else if (spaceAbove > bubbleH + 20) {
|
||
top = rect.top - bubbleH - 14;
|
||
} else {
|
||
// Nichts passt vertikal — Bubble nach rechts neben das Element.
|
||
top = Math.max(20, rect.top);
|
||
}
|
||
left = Math.max(20, Math.min(window.innerWidth - 400, rect.left));
|
||
bubble.style.top = top + 'px';
|
||
bubble.style.left = left + 'px';
|
||
}
|
||
|
||
function showStep(i) {
|
||
if (i < 0 || i >= _resolvedSteps.length) {
|
||
gwoeTourEnd();
|
||
return;
|
||
}
|
||
_tourIdx = i;
|
||
const step = _resolvedSteps[i];
|
||
const overlay = $('gwoe-tour-overlay');
|
||
overlay.hidden = false;
|
||
overlay.setAttribute('aria-hidden', 'false');
|
||
|
||
$('gwoe-tour-step').textContent = `Schritt ${i + 1} / ${_resolvedSteps.length}`;
|
||
$('gwoe-tour-title').textContent = step.title;
|
||
$('gwoe-tour-text').textContent = step.text;
|
||
$('gwoe-tour-prev').disabled = (i === 0);
|
||
$('gwoe-tour-next').textContent = (i === _resolvedSteps.length - 1)
|
||
? 'Fertig ✓' : 'Weiter →';
|
||
|
||
// Element ins Sichtfeld scrollen, dann positionieren.
|
||
step.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
setTimeout(() => positionBubble(step.el), 350);
|
||
|
||
// TTS sprechen lassen
|
||
speak(step.title + '. ' + step.text);
|
||
}
|
||
|
||
// Globale Schnittstelle
|
||
window.gwoeTourStart = function () {
|
||
resolveSteps();
|
||
if (_resolvedSteps.length === 0) {
|
||
console.warn('Tour: keine sichtbaren Stationen.');
|
||
return;
|
||
}
|
||
_tourIdx = 0;
|
||
showStep(0);
|
||
document.body.style.overflow = 'hidden'; // Page-Scroll blockieren
|
||
};
|
||
|
||
window.gwoeTourEnd = function () {
|
||
const overlay = $('gwoe-tour-overlay');
|
||
overlay.hidden = true;
|
||
overlay.setAttribute('aria-hidden', 'true');
|
||
document.body.style.overflow = '';
|
||
stopSpeak();
|
||
};
|
||
|
||
window.gwoeTourNext = function () {
|
||
if (_tourIdx >= _resolvedSteps.length - 1) {
|
||
gwoeTourEnd();
|
||
return;
|
||
}
|
||
showStep(_tourIdx + 1);
|
||
};
|
||
|
||
window.gwoeTourPrev = function () {
|
||
if (_tourIdx <= 0) return;
|
||
showStep(_tourIdx - 1);
|
||
};
|
||
|
||
window.gwoeTourToggleMute = function () {
|
||
_tourMuted = !_tourMuted;
|
||
$('gwoe-tour-mute-icon').textContent = _tourMuted ? '🔇' : '🔊';
|
||
$('gwoe-tour-mute-label').textContent = _tourMuted ? 'Stimme aus' : 'Stimme an';
|
||
if (_tourMuted) stopSpeak();
|
||
else if (_resolvedSteps[_tourIdx]) {
|
||
const s = _resolvedSteps[_tourIdx];
|
||
speak(s.title + '. ' + s.text);
|
||
}
|
||
};
|
||
|
||
// ESC schließt; Klick auf Overlay (außerhalb der Bubble) auch.
|
||
document.addEventListener('keydown', (e) => {
|
||
const overlay = $('gwoe-tour-overlay');
|
||
if (!overlay || overlay.hidden) return;
|
||
if (e.key === 'Escape') gwoeTourEnd();
|
||
else if (e.key === 'ArrowRight') gwoeTourNext();
|
||
else if (e.key === 'ArrowLeft') gwoeTourPrev();
|
||
});
|
||
document.addEventListener('click', (e) => {
|
||
const overlay = $('gwoe-tour-overlay');
|
||
if (!overlay || overlay.hidden) return;
|
||
// Overlay-Klick hinter dem Spotlight oder Bubble → Tour-Ende
|
||
if (e.target === overlay) gwoeTourEnd();
|
||
});
|
||
|
||
// Bei Resize: aktive Bubble + Spotlight neu positionieren
|
||
window.addEventListener('resize', () => {
|
||
if (!$('gwoe-tour-overlay').hidden && _resolvedSteps[_tourIdx]) {
|
||
positionBubble(_resolvedSteps[_tourIdx].el);
|
||
}
|
||
});
|
||
|
||
// Beim Verlassen der Page: Sprachausgabe stoppen.
|
||
window.addEventListener('pagehide', stopSpeak);
|
||
|
||
// Voices laden — manche Browser füllen sie asynchron.
|
||
if ('speechSynthesis' in window) {
|
||
window.speechSynthesis.onvoiceschanged = () => { /* trigger preload */ };
|
||
window.speechSynthesis.getVoices();
|
||
}
|
||
})();
|
||
</script>
|