Quellenübersicht: Klick auf Quelle klappt Artikel-Liste auf

Quellen-Boxen waren bisher reine Anzeige. Jetzt sind sie klickbar:
beim Klick erscheint direkt unter der Box (über die volle Grid-Breite)
eine Liste der Artikel-Headlines dieser Quelle, jede mit Link zum
Originalartikel. Mutual-exclusive — Klick auf eine andere Quelle
schließt die vorherige automatisch.

- components.js: Item bekommt data-source, onclick + Tastatur-Support
  (Enter/Space), aria-expanded.
- app.js: toggleSourceOverviewDetail filtert _currentArticles nach
  Quelle, sortiert chronologisch absteigend, fügt das Detail-Element
  via insertAdjacentElement direkt nach dem geklickten Item ein.
- CSS: aktiver Item-Status (Glow + Tint), Detail-Block mit
  grid-column 1/-1 (volle Breite) + max-height 320px scrollbar bei
  vielen Artikeln + dezente Slide-In-Animation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
2026-05-01 17:57:48 +02:00
Ursprung a08df3d121
Commit a2610d0094
4 geänderte Dateien mit 119 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -866,6 +866,54 @@ const App = {
}
},
/** Klick auf eine Quellen-Box: Liste der Artikel inline aufklappen (mutual-exclusive). */
toggleSourceOverviewDetail(el) {
if (!el) return;
const grid = el.parentElement;
if (!grid) return;
const sourceName = el.dataset.source || '';
const wasActive = el.classList.contains('active');
// Alle anderen schliessen + bestehendes Detail entfernen
grid.querySelectorAll('.source-overview-item.active').forEach(it => {
it.classList.remove('active');
it.setAttribute('aria-expanded', 'false');
});
const existingDetail = grid.querySelector('.source-overview-detail');
if (existingDetail) existingDetail.remove();
// Wenn das geklickte Item bereits aktiv war: nur schliessen
if (wasActive) return;
// Neues Detail einfuegen direkt nach dem geklickten Item
el.classList.add('active');
el.setAttribute('aria-expanded', 'true');
const articles = (this._currentArticles || [])
.filter(a => (a.source || 'Unbekannt') === sourceName)
.sort((a, b) => {
const ta = new Date(a.collected_at || a.published_at || 0).getTime();
const tb = new Date(b.collected_at || b.published_at || 0).getTime();
return tb - ta;
});
const detail = document.createElement('div');
detail.className = 'source-overview-detail';
if (articles.length === 0) {
detail.innerHTML = '<div class="source-overview-detail-empty">Keine Artikel gefunden.</div>';
} else {
const items = articles.map(a => {
const headline = UI.escape(a.headline_de || a.headline || '(ohne Titel)');
if (a.source_url) {
return `<li><a href="${UI.escape(a.source_url)}" target="_blank" rel="noopener">${headline}</a></li>`;
}
return `<li>${headline}</li>`;
}).join('');
detail.innerHTML = `<ul class="source-overview-detail-list">${items}</ul>`;
}
el.insertAdjacentElement('afterend', detail);
},
/** Restliche Artikel seitenweise im Hintergrund nachladen und in _currentArticles mergen. */
async _loadRemainingArticlesInBackground(incidentId) {
const BATCH = 500;

Datei anzeigen

@@ -971,8 +971,9 @@ const UI = {
html += '<div class="source-overview-grid">';
data.sources.forEach(s => {
const langs = (s.languages || ['de']).map(l => (l || 'de').toUpperCase()).join('/');
html += `<div class="source-overview-item">
<span class="source-overview-name">${this.escape(s.source || 'Unbekannt')}</span>
const sourceName = this.escape(s.source || 'Unbekannt');
html += `<div class="source-overview-item" data-source="${sourceName}" tabindex="0" role="button" aria-expanded="false" onclick="App.toggleSourceOverviewDetail(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();App.toggleSourceOverviewDetail(this);}">
<span class="source-overview-name">${sourceName}</span>
<span class="source-overview-lang">${langs}</span>
<span class="source-overview-count">${s.article_count}</span>
</div>`;