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:
parent
70d9790b4b
commit
c4f8ce398a
10
app/main.py
10
app/main.py
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 ≥ 7/10)</span>
|
||||
{% endif %}
|
||||
{% if _has_opp %}
|
||||
<span><span class="v2-marker-icon v2-marker-opp">!</span> Opportunismus: Ja trotz schwacher Wahlprogramm-Übereinstimmung (WP < 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 titleEl = document.getElementById('v2-matrix-field-title');
|
||||
var textEl = document.getElementById('v2-matrix-field-text');
|
||||
var modal = document.getElementById('v2-matrix-field-modal');
|
||||
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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user