Tutorial: Spotlight und Bubble auf sichtbaren Viewport-Bereich beschraenken

- Spotlight wird auf den sichtbaren Teil des Elements geclippt statt
  ueber den Viewport hinauszuragen
- Bubble-Position nutzt sichtbaren Elementbereich statt voller Rect
- Demo-Summary-Text gekuerzt, damit er in die Lagebild-Kachel passt
- Behebt das Problem, dass bei grossen Kacheln (Lagebild, Timeline, Karte)
  die Sprechblase ausserhalb des sichtbaren Bereichs landete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 15:04:02 +01:00
Ursprung b33e635746
Commit 5289bbf29b
2 geänderte Dateien mit 49 neuen und 31 gelöschten Zeilen

Datei anzeigen

@@ -764,7 +764,7 @@
<script src="/static/js/api_network.js?v=20260316a"></script> <script src="/static/js/api_network.js?v=20260316a"></script>
<script src="/static/js/network-graph.js?v=20260316a"></script> <script src="/static/js/network-graph.js?v=20260316a"></script>
<script src="/static/js/app_network.js?v=20260316a"></script> <script src="/static/js/app_network.js?v=20260316a"></script>
<script src="/static/js/tutorial.js?v=20260316c"></script> <script src="/static/js/tutorial.js?v=20260316d"></script>
<script src="/static/js/chat.js?v=20260316e"></script> <script src="/static/js/chat.js?v=20260316e"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script> <script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>

Datei anzeigen

@@ -49,19 +49,18 @@ const Tutorial = {
+ '</div>' + '</div>'
+ '</div>', + '</div>',
_DEMO_SUMMARY: '<p>Am Morgen des 16. März 2026 kam es im Hamburger Hafen zu einer schweren Explosion ' _DEMO_SUMMARY: '<p>Am 16. März 2026 kam es im Hamburger Hafen zu einer Explosion '
+ 'in einem Containerterminal am Burchardkai. Nach übereinstimmenden Berichten von ' + 'im Containerterminal Burchardkai '
+ '<span class="source-ref" data-index="1" title="dpa">dpa [1]</span>, ' + '<span class="source-ref" data-index="1" title="dpa">[1]</span> '
+ '<span class="source-ref" data-index="2" title="Reuters">Reuters [2]</span> und ' + '<span class="source-ref" data-index="2" title="Reuters">[2]</span>. '
+ '<span class="source-ref" data-index="3" title="NDR">NDR [3]</span> ' + 'Die Detonation ereignete sich gegen 06:45 Uhr in einem Gefahrgutbereich. '
+ 'ereignete sich die Detonation gegen 06:45 Uhr Ortszeit in einem Lagerbereich für Gefahrgut.</p>' + 'Mindestens 12 Personen wurden verletzt '
+ '<p>Die Hamburger Feuerwehr ist mit einem Großaufgebot vor Ort. Laut ' + '<span class="source-ref" data-index="3" title="NDR">[3]</span>.</p>'
+ '<span class="source-ref" data-index="4" title="Hamburger Abendblatt">Hamburger Abendblatt [4]</span> ' + '<p>Die Feuerwehr ist mit einem Großaufgebot vor Ort. Der Hafenbetrieb im betroffenen Terminal wurde '
+ 'wurden mindestens 12 Personen verletzt, davon 3 schwer. Die Ursache der Explosion ist noch unklar. ' + 'vorübergehend eingestellt '
+ 'Die Polizei Hamburg hat den Bereich weiträumig abgesperrt.</p>' + '<span class="source-ref" data-index="4" title="HPA">[4]</span>. '
+ '<p>Der Hamburger Hafen, Europas drittgrößter Seehafen, hat den Betrieb im betroffenen Terminal vorübergehend ' + 'Die Ursache ist noch unklar '
+ 'eingestellt. Auswirkungen auf den Schiffsverkehr werden derzeit geprüft ' + '<span class="source-ref" data-index="5" title="Hamburger Abendblatt">[5]</span>.</p>',
+ '<span class="source-ref" data-index="5" title="Hamburg Port Authority">HPA [5]</span>.</p>',
_DEMO_FACTCHECKS: [ _DEMO_FACTCHECKS: [
{ status: 'confirmed', icon: '&#10003;', claim: 'Eine Explosion ereignete sich am 16.03.2026 gegen 06:45 Uhr im Hamburger Hafen.', sources: 5 }, { status: 'confirmed', icon: '&#10003;', claim: 'Eine Explosion ereignete sich am 16.03.2026 gegen 06:45 Uhr im Hamburger Hafen.', sources: 5 },
@@ -901,13 +900,21 @@ const Tutorial = {
} }
var rect = el.getBoundingClientRect(); var rect = el.getBoundingClientRect();
var vw = window.innerWidth;
var vh = window.innerHeight;
var pad = 8; var pad = 8;
var s = this._els.spotlight.style;
s.top = (rect.top - pad) + 'px'; // Sichtbaren Bereich berechnen (auf Viewport beschränkt)
s.left = (rect.left - pad) + 'px'; var top = Math.max(rect.top, 0) - pad;
s.width = (rect.width + pad * 2) + 'px'; var left = Math.max(rect.left, 0) - pad;
s.height = (rect.height + pad * 2) + 'px'; var bottom = Math.min(rect.bottom, vh) + pad;
var right = Math.min(rect.right, vw) + pad;
var s = this._els.spotlight.style;
s.top = top + 'px';
s.left = left + 'px';
s.width = (right - left) + 'px';
s.height = (bottom - top) + 'px';
s.borderRadius = '8px'; s.borderRadius = '8px';
s.opacity = '1'; s.opacity = '1';
}, },
@@ -988,6 +995,14 @@ const Tutorial = {
var vh = window.innerHeight; var vh = window.innerHeight;
var gap = 16; var gap = 16;
// Sichtbaren Bereich des Elements berechnen (auf Viewport beschraenkt)
var visTop = Math.max(rect.top, 0);
var visBottom = Math.min(rect.bottom, vh);
var visLeft = Math.max(rect.left, 0);
var visRight = Math.min(rect.right, vw);
var visCenterY = (visTop + visBottom) / 2;
var visCenterX = (visLeft + visRight) / 2;
// Reset transform // Reset transform
bubble.style.transform = ''; bubble.style.transform = '';
bubble.style.width = bw + 'px'; bubble.style.width = bw + 'px';
@@ -996,30 +1011,33 @@ const Tutorial = {
var pos = step.position || 'bottom'; var pos = step.position || 'bottom';
var bubbleHeight = 300; var bubbleHeight = 300;
if (pos === 'bottom' && (rect.bottom + gap + bubbleHeight > vh)) pos = 'top'; if (pos === 'bottom' && (visBottom + gap + bubbleHeight > vh)) pos = 'top';
if (pos === 'top' && (rect.top - gap - bubbleHeight < 0)) pos = 'bottom'; if (pos === 'top' && (visTop - gap - bubbleHeight < 0)) pos = 'bottom';
if (pos === 'right' && (rect.right + gap + bw > vw)) pos = 'left'; if (pos === 'right' && (visRight + gap + bw > vw)) pos = 'left';
if (pos === 'left' && (rect.left - gap - bw < 0)) pos = 'right'; if (pos === 'left' && (visLeft - gap - bw < 0)) pos = 'right';
bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos; bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos;
// Bubble-Top immer im sichtbaren Bereich halten
var clampTop = function(t) { return Math.max(8, Math.min(t, vh - bubbleHeight - 8)); };
switch (pos) { switch (pos) {
case 'bottom': case 'bottom':
bubble.style.top = (rect.bottom + gap) + 'px'; bubble.style.top = Math.min(visBottom + gap, vh - bubbleHeight - 8) + 'px';
bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px'; bubble.style.left = Math.max(8, Math.min(visCenterX - bw / 2, vw - bw - 8)) + 'px';
break; break;
case 'top': case 'top':
bubble.style.top = (rect.top - gap) + 'px'; bubble.style.top = Math.max(visTop - gap, 8) + 'px';
bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px'; bubble.style.left = Math.max(8, Math.min(visCenterX - bw / 2, vw - bw - 8)) + 'px';
bubble.style.transform = 'translateY(-100%)'; bubble.style.transform = 'translateY(-100%)';
break; break;
case 'right': case 'right':
bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px'; bubble.style.top = clampTop(visCenterY - bubbleHeight / 2) + 'px';
bubble.style.left = (rect.right + gap) + 'px'; bubble.style.left = Math.min(visRight + gap, vw - bw - 8) + 'px';
break; break;
case 'left': case 'left':
bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px'; bubble.style.top = clampTop(visCenterY - bubbleHeight / 2) + 'px';
bubble.style.left = (rect.left - gap - bw) + 'px'; bubble.style.left = Math.max(visLeft - gap - bw, 8) + 'px';
break; break;
} }
}, },