feat: Matrix-Klick mit Antrags-Begründung + Vote-Block-Verbesserungen

Matrix-Info-Modal:
- _row_to_detail liefert label+aspect je Matrix-Cell ans Frontend
- Modal zeigt antragsspezifische Bewertung (Label + ausformulierte
  Begründung + Rating-Chip) UND die allgemeine Felderklärung
  (was misst dieses Feld?). v1-Verhalten wiederhergestellt.

Abstimmungsergebnis (Vote-Block):
- Outlink-Pfeil ↗ ist jetzt ein klickbares <a> auf v.quelle_url
  (target=_blank). Vorher: Span ohne Link, Pfeil tat nichts.
- Marker ⚠ (Heuchelei) und ! (Opportunismus) bekommen sichtbare
  Hover-Affordanz (Hintergrund + dotted-border on focus). Native
  title= bleibt fuer Screenreader-/Tooltip aktiv. tabindex=0+role=button
  macht sie keyboard-erreichbar.
- Legende unter dem Vote-Pill-Block erklaert die Marker beim
  ersten Auftreten in einer Liste — wird nur eingeblendet wenn auf
  dem Block mindestens ein Marker tatsaechlich vorkommt.
- Vote-Pills via Klassen v2-vote-pill/-ja/-nein/-enth statt
  Inline-Styles (CD-Annaeherung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 09:33:53 +02:00
parent 70d9790b4b
commit c4f8ce398a
3 changed files with 195 additions and 16 deletions

View File

@ -434,7 +434,15 @@ def _row_to_detail(row):
if rating_normalized < -5: rating_normalized = -5
if rating_normalized > 5: rating_normalized = 5
symbol = cell.get("symbol", "")
matrix_dict[field] = {"rating": rating_normalized, "symbol": symbol}
matrix_dict[field] = {
"rating": rating_normalized,
"symbol": symbol,
# Antrags-spezifische Beschriftung + Begruendung fuer Click-Info.
# (LLM liefert pro Feld einen prägnanten Titel und einen Satz
# zur Begründung; in v1 wurde das ausgeschrieben angezeigt.)
"label": cell.get("label", "") or "",
"aspect": cell.get("aspect", "") or "",
}
# Fallback fuer ist_antragsteller / ist_regierung wenn LLM-Output sie
# nicht gesetzt hat: aus den Drucksachen-Metadaten ableiten.

View File

@ -1174,3 +1174,89 @@ body.v2 .v2-admin-badge.running {
background: rgba(0,120,100,.18);
color: var(--ecg-teal);
}
/* ── Abstimmungsergebnis: Pills, Marker, Legende ───────────────────── */
.v2-vote-pill {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
margin-right: 3px;
}
.v2-vote-ja { background: color-mix(in srgb, #2da44e 15%, transparent); color: #1a7f37; }
.v2-vote-nein { background: color-mix(in srgb, #cf222e 15%, transparent); color: #a40e26; }
.v2-vote-enth { background: color-mix(in srgb, #6e7781 15%, transparent); color: #57606a; }
/* Marker ( Heuchelei / ! Opportunismus): kompakt, mit klarer
Hover-/Focus-Affordanz fuer Bürger:innen, die Tooltip-Konventionen
nicht kennen. Native title= bleibt fuer Screenreader/Tooltip aktiv. */
.v2-vote-marker {
display: inline-block;
margin-left: 4px;
padding: 0 4px;
border-radius: 8px;
cursor: help;
font-weight: 700;
line-height: 1.4;
border-bottom: 1px dotted transparent;
transition: background 0.12s ease, border-color 0.12s ease;
}
.v2-vote-marker:hover,
.v2-vote-marker:focus-visible {
outline: none;
border-bottom-color: currentColor;
}
.v2-marker-heuchelei {
color: #a40e26;
background: rgba(207, 34, 46, 0.08);
}
.v2-marker-heuchelei:hover,
.v2-marker-heuchelei:focus-visible {
background: rgba(207, 34, 46, 0.18);
}
.v2-marker-opp {
color: #bf8700;
background: rgba(191, 135, 0, 0.10);
font-style: italic;
}
.v2-marker-opp:hover,
.v2-marker-opp:focus-visible {
background: rgba(191, 135, 0, 0.20);
}
/* Legende unter den Vote-Pills: erklärt sichtbar, was die Marker
bedeuten. Wird nur eingeblendet, wenn auf dem Block tatsaechlich
ein Marker vorkommt (Template-Logik). */
.v2-marker-legend {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--hairline);
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--ecg-dark);
opacity: 0.78;
line-height: 1.5;
}
.v2-marker-legend .v2-marker-icon {
display: inline-block;
padding: 0 4px;
border-radius: 8px;
font-weight: 700;
margin-right: 2px;
}
.v2-marker-legend .v2-marker-heuchelei {
background: rgba(207, 34, 46, 0.12);
color: #a40e26;
}
.v2-marker-legend .v2-marker-opp {
background: rgba(191, 135, 0, 0.12);
color: #bf8700;
font-style: italic;
}
/* Plenarprotokoll-Outlink in der Header-Zeile */
.v2-quelle-link:hover {
background: rgba(0, 157, 165, 0.08);
}

View File

@ -293,9 +293,16 @@
<span style="font-family:var(--font-display);font-size:14px;font-weight:700;color:{{ ergebnis_color.get(v.ergebnis, '#6e7781') }};">
{{ v.ergebnis | capitalize }}{% if v.einstimmig %} · einstimmig{% endif %}
</span>
<span style="font-family:var(--font-mono);font-size:10px;opacity:0.6;" title="{% if v.quelle_url %}{{ v.quelle_url }}{% endif %}">
{{ v.quelle_protokoll }}{% if v.quelle_url %} ↗{% endif %}
</span>
{% if v.quelle_url %}
<a href="{{ v.quelle_url }}" target="_blank" rel="noopener"
class="v2-quelle-link"
style="font-family:var(--font-mono);font-size:10px;color:var(--ecg-blue);text-decoration:none;border-bottom:1px solid rgba(0,157,165,0.35);"
title="Plenarprotokoll im neuen Tab öffnen ({{ v.quelle_url }})">
{{ v.quelle_protokoll }} <span aria-hidden="true"></span>
</a>
{% else %}
<span style="font-family:var(--font-mono);font-size:10px;opacity:0.6;">{{ v.quelle_protokoll }}</span>
{% endif %}
</div>
{% if v.fraktionen_ja or v.fraktionen_nein or v.fraktionen_enthaltung %}
{# Mehrheits-Bar: Fraktions-Anzahlen pro Lager als Stacked Bar #}
@ -322,7 +329,7 @@
<div><span style="color:#2da44e;font-weight:700;">Ja:</span>
{% for f in v.fraktionen_ja %}
{% set _opp_match = opportunismus_score(f, antrag.fraktions_scores) %}
<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#2da44e 15%,transparent);color:#1a7f37;border-radius:3px;margin-right:3px;">{{ f }}{% if _opp_match is not none %}<span style="margin-left:4px;cursor:help;font-style:italic;color:#bf8700;" title="Diese Fraktion stimmte mit Ja, obwohl der Antrag nicht zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_opp_match) }}/10).">!</span>{% endif %}</span>
<span class="v2-vote-pill v2-vote-ja">{{ f }}{% if _opp_match is not none %}<span class="v2-vote-marker v2-marker-opp" tabindex="0" role="button" aria-label="Opportunismus-Hinweis: Ja trotz schwacher Wahlprogramm-Übereinstimmung" title="Opportunismus-Marker — Diese Fraktion stimmte mit Ja, obwohl der Antrag nicht zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_opp_match) }}/10).">!</span>{% endif %}</span>
{% endfor %}
</div>
{% endif %}
@ -330,16 +337,38 @@
<div><span style="color:#cf222e;font-weight:700;">Nein:</span>
{% for f in v.fraktionen_nein %}
{% set _wp_match = heuchelei_score(f, antrag.fraktions_scores) %}
<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#cf222e 15%,transparent);color:#a40e26;border-radius:3px;margin-right:3px;">{{ f }}{% if _wp_match is not none %}<span style="margin-left:4px;cursor:help;" title="Diese Fraktion stimmte mit Nein, obwohl der Antrag gut zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_wp_match) }}/10)."></span>{% endif %}</span>
<span class="v2-vote-pill v2-vote-nein">{{ f }}{% if _wp_match is not none %}<span class="v2-vote-marker v2-marker-heuchelei" tabindex="0" role="button" aria-label="Heuchelei-Hinweis: Nein trotz hoher Wahlprogramm-Übereinstimmung" title="Heuchelei-Marker — Diese Fraktion stimmte mit Nein, obwohl der Antrag gut zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_wp_match) }}/10)."></span>{% endif %}</span>
{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_enthaltung %}
<div><span style="color:#6e7781;font-weight:700;cursor:help;border-bottom:1px dotted currentColor;" title="Enth. — Enthaltung: weder Zustimmung noch Ablehnung.">Enth.:</span>
{% for f in v.fraktionen_enthaltung %}<span style="display:inline-block;padding:1px 6px;background:color-mix(in srgb,#6e7781 15%,transparent);color:#57606a;border-radius:3px;margin-right:3px;">{{ f }}</span>{% endfor %}
{% for f in v.fraktionen_enthaltung %}<span class="v2-vote-pill v2-vote-enth">{{ f }}</span>{% endfor %}
</div>
{% endif %}
</div>
{# Marker-Legende: nur einblenden wenn auf dem Vote-Block ein
⚠- oder !-Marker tatsächlich vorkam, sonst würde das Bord der
Bürgerin Erklärungstext zeigen, wofür sie kein Symbol sieht. #}
{% set _has_heuchelei = false %}
{% for f in (v.fraktionen_nein or []) %}
{% if heuchelei_score(f, antrag.fraktions_scores) is not none %}{% set _has_heuchelei = true %}{% endif %}
{% endfor %}
{% set _has_opp = false %}
{% for f in (v.fraktionen_ja or []) %}
{% if opportunismus_score(f, antrag.fraktions_scores) is not none %}{% set _has_opp = true %}{% endif %}
{% endfor %}
{% if _has_heuchelei or _has_opp %}
<div class="v2-marker-legend">
{% if _has_heuchelei %}
<span><span class="v2-marker-icon v2-marker-heuchelei"></span> Heuchelei: Nein trotz hoher Wahlprogramm-Übereinstimmung (WP&nbsp;&nbsp;7/10)</span>
{% endif %}
{% if _has_opp %}
<span><span class="v2-marker-icon v2-marker-opp">!</span> Opportunismus: Ja trotz schwacher Wahlprogramm-Übereinstimmung (WP&nbsp;&lt;&nbsp;3/10)</span>
{% endif %}
</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
@ -563,19 +592,42 @@
z-index:9100;align-items:center;justify-content:center;"
onclick="if(event.target===this)this.style.display='none'">
<div style="background:var(--ecg-card-bg,#fff);border:1px solid var(--ecg-border,#ddd);
border-radius:8px;padding:28px 32px;min-width:280px;max-width:480px;
border-radius:8px;padding:24px 28px;min-width:300px;max-width:540px;
font-family:var(--font-sans);font-size:14px;color:var(--ecg-dark);
line-height:1.55;box-shadow:0 8px 32px rgba(0,0,0,0.18);">
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:6px;">
<strong id="v2-matrix-field-title"
style="font-family:var(--font-display,inherit);font-size:16px;font-weight:900;
color:var(--ecg-teal,#009da5);letter-spacing:0.03em;"></strong>
<span id="v2-matrix-field-rating"
style="font-family:var(--font-mono);font-size:12px;font-weight:700;
padding:2px 8px;border-radius:3px;white-space:nowrap;"></span>
<button onclick="document.getElementById('v2-matrix-field-modal').style.display='none'"
style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--ecg-dark);
opacity:0.55;padding:0;line-height:1;"
opacity:0.55;padding:0;line-height:1;margin-left:auto;"
aria-label="Schließen">×</button>
</div>
<p id="v2-matrix-field-text" style="margin:0;"></p>
{# Antrags-spezifischer Block: Label + ausformulierte Begründung #}
<div id="v2-matrix-field-antrag" style="display:none;margin:14px 0 0;">
<div style="font-family:var(--font-mono);font-size:10px;text-transform:uppercase;
letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:4px;">
Bewertung in diesem Antrag
</div>
<div id="v2-matrix-field-label"
style="font-weight:700;font-size:14px;margin-bottom:4px;"></div>
<div id="v2-matrix-field-aspect"
style="font-size:13.5px;line-height:1.55;"></div>
</div>
{# Allgemeine Erklärung des Matrix-Felds (als Hintergrund) #}
<div style="margin-top:16px;padding-top:12px;border-top:1px solid var(--hairline);">
<div style="font-family:var(--font-mono);font-size:10px;text-transform:uppercase;
letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:4px;">
Was misst dieses Feld?
</div>
<p id="v2-matrix-field-text" style="margin:0;font-size:13px;line-height:1.55;opacity:0.85;"></p>
</div>
</div>
</div>
@ -603,15 +655,48 @@ document.addEventListener('keydown', function (e) {
{% if matrix_explanations is defined %}
<script>
window._v2MatrixExplanations = {{ matrix_explanations | tojson }};
window._v2MatrixCells = {{ (antrag.matrix if antrag is defined and antrag else {}) | tojson }};
window.v2ShowMatrixFieldInfo = function(field) {
var explains = window._v2MatrixExplanations || {};
var text = explains[field] || '';
var cells = window._v2MatrixCells || {};
var generalText = explains[field] || '';
var cell = cells[field] || {};
var titleEl = document.getElementById('v2-matrix-field-title');
var ratingEl = document.getElementById('v2-matrix-field-rating');
var antragEl = document.getElementById('v2-matrix-field-antrag');
var labelEl = document.getElementById('v2-matrix-field-label');
var aspectEl = document.getElementById('v2-matrix-field-aspect');
var textEl = document.getElementById('v2-matrix-field-text');
var modal = document.getElementById('v2-matrix-field-modal');
if (!modal) return;
if (titleEl) titleEl.textContent = 'Feld ' + field;
if (textEl) textEl.textContent = text || '(Keine Erklärung vorhanden)';
/* Rating-Chip mit gleichem Farbcode wie matrix_mini-Klassen */
if (ratingEl) {
var r = (cell.rating != null) ? cell.rating : 0;
var sym = cell.symbol || (r >= 4 ? '++' : r >= 1 ? '+' : r === 0 ? '○' : r <= -4 ? '' : '');
var bg = '#eee', fg = '#555';
if (r >= 4) { bg = 'rgba(0,157,165,0.20)'; fg = '#0d6f76'; }
else if (r >= 1) { bg = 'rgba(0,157,165,0.10)'; fg = '#0d6f76'; }
else if (r === 0) { bg = 'rgba(180,180,180,0.18)'; fg = '#666'; }
else if (r >= -3) { bg = 'rgba(207,34,46,0.12)'; fg = '#a40e26'; }
else { bg = 'rgba(207,34,46,0.22)'; fg = '#a40e26'; }
ratingEl.style.background = bg;
ratingEl.style.color = fg;
ratingEl.textContent = sym + ' ' + (r > 0 ? '+' : '') + r;
}
/* Antrags-spezifischer Block: nur sichtbar wenn LLM Begründung lieferte */
var hasAntrag = (cell.label || cell.aspect);
if (antragEl) antragEl.style.display = hasAntrag ? 'block' : 'none';
if (labelEl) labelEl.textContent = cell.label || '';
if (aspectEl) aspectEl.textContent = cell.aspect || '';
/* Allgemeine Erklärung des Felds */
if (textEl) textEl.textContent = generalText || '(Keine allgemeine Erklärung vorhanden)';
modal.style.display = 'flex';
};
</script>