gwoe-antragspruefer/app/templates/v3/components/tour.html
Dotty Dotter e31ee1ad07 feat(tour): Welcome-Banner + Tour auf Startseite, Logo-Klick zur Startseite
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.
2026-05-09 02:47:04 +02:00

329 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{# ─────────────────────────────────────────────────────────────────────
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>