This commit is contained in:
team3
2026-07-27 13:42:59 +02:00
parent 41185eaccc
commit 23c908534f
13 changed files with 274 additions and 77 deletions

View File

@@ -65,7 +65,7 @@ Spezifische Hinweise für Features
<!-- testen -->
- Vollmodus öffnen
- Klick auf Regeln
- Inhalt anpassen
- Inhalt angepasst
<!-- funktion -->
Titel anpassen
<!-- regeln -->
@@ -74,6 +74,7 @@ Titel anpassen
- Vollmodus öffnen
- Klick auf Titel
- Inhalt ändern
- Titel aktualisiert
<!-- funktion -->
Ziel anpassen
<!-- ziel -->
@@ -82,3 +83,4 @@ Beschreibt kurz das Kernziel vom Feature
- Vollmodus öffnen
- Klick auf Ziel
- Inhalt anpassen
- Ziel angepasst

View File

@@ -32,7 +32,7 @@ Funktion erstellen
- RMB auf Featurecard
- Klick auf "Funktion erstellen"
- Namen im Modal eingeben
- Bestätigen
- Funktion erstellt
<!-- funktion -->
Regeln anpassen
<!-- ziel -->

View File

@@ -50,10 +50,10 @@ Prüfung ausführen
- Prüf-Icon in Sidebar Navigation
<!-- testen -->
- LMB auf Icon
- Prüfung wird gestartet
- Prüfung gestartet
- Rechte Sidebar geöffnet
- Fortschritt unter Icon angezeigt
- Probleme werden live gelistet
- Probleme live gelistet
<!-- funktion -->
Stellen einzeln prüfen
<!-- ziel -->

View File

@@ -32,7 +32,7 @@ Projekt aufrufen
Projekt entfernen
<!-- regeln -->
- x-Icon
- Auch Features entfernen
- Features mit entfernen
<!-- testen -->
- RMB auf Projektname
- Klick auf "Projekt entfernen"
@@ -48,3 +48,4 @@ Projekt erstellen
- Klick auf "Projekt erstellen"
- Im Modal Namen eingeben
- Bestätigen
- Projekt erstellt

View File

@@ -107,15 +107,30 @@ onMounted(async () => {
</template>
<style scoped>
/* App-Shell: feste Höhe, drei eigene Scrollbereiche. Vorher wanderten die
Leisten beim Scrollen mit nach oben weg. */
.raster {
display: grid;
grid-template-columns: 220px 1fr;
min-height: 100vh;
grid-template-columns: 15rem 1fr;
height: 100vh;
overflow: hidden;
}
.raster.pruefung { grid-template-columns: 220px 1fr 260px; }
.raster.pruefung { grid-template-columns: 15rem 1fr 20rem; }
main {
padding: 1.2rem 1.5rem;
min-width: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
/* Schmale Fenster: die Projektliste legt sich als Leiste nach oben */
@media (max-width: 46rem) {
.raster, .raster.pruefung {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.raster.pruefung { grid-template-rows: auto 1fr auto; }
main { padding: 0.9rem 1rem; }
}
.leer {
color: var(--dezent);

View File

@@ -2,11 +2,11 @@
--bg: #f6f7f9;
--panel: #ffffff;
--text: #1a202c;
--dezent: #64748b;
--dezent: #556070; /* dunkler als slate-500: 4.34 war unter dem Lesbarkeitsminimum */
--rand: #e2e8f0;
--akzent: #2563eb;
--akzent-text: #ffffff;
--gruen: #16a34a;
--gruen: #166534; /* auf gruen-bg reichte green-600 nicht (3.0) */
--gruen-bg: #dcfce7;
--gelb: #b45309;
--gelb-bg: #fef3c7;
@@ -75,16 +75,24 @@ input, textarea {
padding: 0.9rem 1.1rem;
}
/* Rahmenlose Icon-Knöpfe (✎ ✕ …) */
/* Rahmenlose Icon-Knöpfe (✎ ✕ …) — mindestens 1.75rem Klickfläche */
.icon-knopf {
border: none;
background: none;
padding: 0.1rem 0.3rem;
padding: 0;
min-width: 1.75rem;
height: 1.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: var(--dezent);
font-size: 0.95rem;
line-height: 1;
}
.icon-knopf:hover { color: var(--text); }
.icon-knopf.rot:hover { color: var(--rot); }
.icon-knopf:hover { color: var(--text); background: var(--bg); }
.icon-knopf.rot:hover { color: var(--rot); background: var(--rot-bg); }
.icon-knopf:focus-visible { outline: 2px solid var(--akzent); outline-offset: 1px; }
.meldung { color: var(--rot); font-size: 0.85rem; }
@@ -135,7 +143,10 @@ input, textarea {
.badge {
font-size: 0.72rem;
font-weight: 600;
padding: 0.1rem 0.5rem;
min-height: 1.5rem;
display: inline-flex;
align-items: center;
padding: 0.1rem 0.6rem;
border-radius: 99px;
text-transform: uppercase;
letter-spacing: 0.04em;

View File

@@ -272,6 +272,7 @@ function alleAblehnen() {
<template>
<article
class="karte"
:data-feature="feature.id"
:class="[s.art, { gross, fehlerhaft: probleme.length }]"
@mousedown.middle.prevent
@auxclick.middle.prevent="wechsleAnsicht"
@@ -306,8 +307,17 @@ function alleAblehnen() {
@click="wechsleAnsicht"
>👁</button>
<span v-if="probleme.length" class="alle">
<button class="icon-knopf" title="Alle annehmen" @click="alleAnnehmen">✓</button>
<button class="icon-knopf rot" title="Alle ablehnen" @click="alleAblehnen">✕</button>
<span class="anzahl">{{ probleme.length }}</span>
<button
class="icon-knopf uebernehmen"
:title="`Alle ${probleme.length} Vorschläge übernehmen`"
@click="alleAnnehmen"
>✓</button>
<button
class="icon-knopf rot"
:title="`Alle ${probleme.length} Vorschläge verwerfen`"
@click="alleAblehnen"
>✕</button>
</span>
</header>
<ProblemHinweis :probleme="problemeAn('titel')" @annehmen="annehmen" @ablehnen="ablehnen" />
@@ -482,9 +492,33 @@ function alleAblehnen() {
.karte.optional { border-color: var(--gruen); }
/* Prüfung: fehlerhafte Karte und Stelle rot */
.karte.fehlerhaft { border-color: var(--rot); }
/* Kurz hervorheben, wenn das Panel hierher springt */
.karte.gesucht { box-shadow: 0 0 0 3px var(--akzent); }
.karte { transition: box-shadow 0.25s; }
.fehler { color: var(--rot); }
.badge.fehler { color: var(--rot); background: var(--rot-bg); }
.alle { white-space: nowrap; }
/* Sammelaktionen der Karte — abgesetzt, damit sie nicht wie die Knöpfe
eines einzelnen Vorschlags aussehen */
.alle {
display: inline-flex;
align-items: center;
gap: 0.15rem;
white-space: nowrap;
margin-left: 0.15rem;
padding-left: 0.4rem;
border-left: 1px solid var(--rand);
}
.anzahl {
font-size: 0.7rem;
font-weight: 700;
color: var(--akzent-text);
background: var(--rot);
border-radius: 99px;
min-width: 1.1rem;
padding: 0.05rem 0.3rem;
text-align: center;
}
.icon-knopf.uebernehmen:hover { color: var(--gruen); background: var(--gruen-bg); }
/* Vollmodus: volle Breite, Funktionen als Boxen in responsiven Spalten,
alle Details offen */
@@ -553,7 +587,9 @@ section.leer { display: none; }
background: none;
color: var(--dezent);
font-size: 0.8rem;
padding: 0.1rem 0;
min-height: 1.5rem;
padding: 0.1rem 0.3rem;
margin-left: -0.3rem;
}
.hinzu:hover { color: var(--akzent); }

View File

@@ -117,10 +117,14 @@ watch(() => props.projekt, laden, { immediate: true })
<style scoped>
.bereich { min-height: 100%; }
/* Spaltenlayout: unterschiedlich hohe Karten stapeln sich ohne Lücken */
/* Spaltenlayout: unterschiedlich hohe Karten stapeln sich ohne Lücken.
Die Breite ist gedeckelt — auf sehr breiten Schirmen klumpten die Karten sonst
links und rechts blieb eine tote Fläche. */
.gitter {
columns: 340px;
columns: 22rem;
column-gap: 0.9rem;
max-width: 100rem;
margin: 0 auto;
}
.gitter > * {
/* inline-block macht die Karte atomar — Firefox fragmentiert sie sonst

View File

@@ -1,80 +1,115 @@
<script setup>
import { vorschlagsArt } from '../composables/pruefung.js'
import { vergleich, vorschlagsArt } from '../composables/pruefung.js'
defineProps({
probleme: { type: Array, required: true },
})
const emit = defineEmits(['annehmen', 'ablehnen'])
// Was der Vorschlag bewirkt, steht nicht in der Antwort — es ergibt sich aus dem
// Vergleich mit der alten Zeile. Bei entfernen steht dort die Zeile, die weggeht.
const ZEICHEN = { ergaenzen: '+', entfernen: '−' }
const zeichen = (p) => ZEICHEN[vorschlagsArt(p)] || ''
const text = (p) => (vorschlagsArt(p) === 'entfernen' ? p.original : p.vorschlag)
// Was das ✓ bewirkt, soll man vorher sehen. Darum die Gegenüberstellung
// statt eines bloßen Ersatztextes.
const TITEL = { ergaenzen: 'Ergänzen', entfernen: 'Streichen', anpassen: 'Ändern' }
const ZEICHEN = { weg: '−', neu: '+', gleich: '' }
</script>
<template>
<div v-for="(p, i) in probleme" :key="i" class="hinweis-block" @click.stop>
<div class="kopf">
<p class="problem">{{ p.problem }}</p>
<span class="art">{{ TITEL[vorschlagsArt(p)] }}</span>
<span class="knoepfe">
<button class="icon-knopf" title="Annehmen" @click="emit('annehmen', p)">✓</button>
<button class="icon-knopf rot" title="Ablehnen" @click="emit('ablehnen', p)">✕</button>
<button class="icon-knopf uebernehmen" title="Übernehmen" @click="emit('annehmen', p)">✓</button>
<button class="icon-knopf rot" title="Verwerfen" @click="emit('ablehnen', p)">✕</button>
</span>
</div>
<p class="vorschlag" :class="vorschlagsArt(p)">
<span v-if="zeichen(p)" class="zeichen">{{ zeichen(p) }}</span>{{ text(p) }}
</p>
<p v-if="p.regel" class="regel" :title="p.regel">{{ p.regel }}</p>
<p class="problem">{{ p.problem }}</p>
<ul class="vergleich">
<li v-for="(z, k) in vergleich(p)" :key="k" :class="z.art">
<span class="zeichen">{{ ZEICHEN[z.art] }}</span><span class="text">{{ z.text }}</span>
</li>
</ul>
<p v-if="p.regel" class="regel">{{ p.regel }}</p>
</div>
</template>
<style scoped>
.hinweis-block {
border-left: 2px solid var(--rot);
background: var(--rot-bg);
border-radius: 0 4px 4px 0;
padding: 0.25rem 0.5rem 0.35rem;
margin: 0.15rem 0 0.3rem;
border: 1px solid var(--rand);
border-left: 3px solid var(--akzent);
background: var(--panel);
border-radius: 0 6px 6px 0;
padding: 0.4rem 0.55rem 0.45rem;
margin: 0.3rem 0 0.4rem;
min-width: 0;
cursor: default;
}
/* Zwei feste Zeilen: Marke und Knöpfe oben, Begründung darunter. Nebeneinander
zerfaserte der Kopf auf schmalen Karten. */
.kopf {
display: flex;
align-items: baseline;
align-items: center;
gap: 0.4rem;
}
.problem {
margin: 0;
flex: 1;
min-width: 0;
font-size: 0.72rem;
.knoepfe { margin-left: auto; }
.art {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 1.35;
color: var(--rot);
}
.knoepfe { white-space: nowrap; }
.vorschlag {
margin: 0.1rem 0 0;
font-size: 0.85rem;
overflow-wrap: anywhere;
white-space: pre-line; /* mehrzeilige Vorschläge bleiben mehrzeilig */
}
.vorschlag.entfernen { text-decoration: line-through; color: var(--dezent); }
/* Die belegte Katalogzeile — macht den Vorschlag nachprüfbar */
.regel {
margin: 0.15rem 0 0;
font-size: 0.72rem;
color: var(--dezent);
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.06em;
color: var(--akzent-text);
background: var(--akzent);
border-radius: 4px;
padding: 0.05rem 0.35rem;
white-space: nowrap;
}
.problem {
margin: 0.15rem 0 0.3rem;
font-size: 0.78rem;
line-height: 1.35;
color: var(--dezent);
}
.knoepfe { display: flex; white-space: nowrap; }
.icon-knopf.uebernehmen:hover { color: var(--gruen); background: var(--gruen-bg); }
/* Gegenüberstellung: was weg ist, was neu ist, was bleibt */
.vergleich {
list-style: none;
margin: 0;
padding: 0;
font-size: 0.85rem;
}
.vergleich li {
display: flex;
gap: 0.15rem;
border-radius: 3px;
padding: 0.02rem 0.15rem;
}
.zeichen {
display: inline-block;
width: 1rem;
font-weight: 600;
text-decoration: none;
flex: none;
width: 0.9rem;
font-weight: 700;
text-align: center;
}
.text { min-width: 0; overflow-wrap: anywhere; }
.vergleich li.weg {
color: var(--rot);
background: var(--rot-bg);
}
.vergleich li.weg .text { text-decoration: line-through; }
.vergleich li.neu {
color: var(--gruen);
background: var(--gruen-bg);
}
.vergleich li.gleich { color: var(--dezent); }
/* Die belegte Katalogzeile — macht den Vorschlag nachprüfbar */
.regel {
margin: 0.35rem 0 0;
padding-top: 0.25rem;
border-top: 1px solid var(--rand);
font-size: 0.72rem;
line-height: 1.35;
color: var(--dezent);
}
</style>

View File

@@ -1,5 +1,21 @@
<script setup>
import { laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '../composables/pruefung.js'
// Nur die erste Zeile — mehrzeilige Stellen liefen hier sonst zu einem Klumpen zusammen
function erste(text) {
const zeilen = (text || '').split('\n').filter(z => z.trim())
if (!zeilen.length) return ''
return zeilen.length > 1 ? `${zeilen[0]} …` : zeilen[0]
}
// Die Liste ist ein Inhaltsverzeichnis: ein Klick führt zur Karte
function springe(p) {
const karte = document.querySelector(`[data-feature="${CSS.escape(p.feature)}"]`)
if (!karte) return
karte.scrollIntoView({ behavior: 'smooth', block: 'center' })
karte.classList.add('gesucht')
setTimeout(() => karte.classList.remove('gesucht'), 1200)
}
</script>
<template>
@@ -22,9 +38,11 @@ import { laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '.
<ul>
<li v-for="(p, i) in probleme" :key="i">
<span class="stelle">{{ p.feature }}<template v-if="p.funktion"> · {{ p.funktion }}</template> · {{ p.feld }}</span>
<span class="problem">{{ p.problem }}</span>
<span class="original">{{ p.original }}</span>
<button class="eintrag" @click="springe(p)">
<span class="stelle">{{ p.feature }}<template v-if="p.funktion"> · {{ p.funktion }}</template> · {{ p.feld }}</span>
<span class="problem">{{ p.problem }}</span>
<span class="original">{{ erste(p.original) }}</span>
</button>
</li>
</ul>
</aside>
@@ -38,7 +56,17 @@ import { laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '.
display: flex;
flex-direction: column;
gap: 0.4rem;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
@media (max-width: 46rem) {
.panel {
border-left: none;
border-top: 1px solid var(--rand);
max-height: 33vh;
}
}
header {
display: flex;
@@ -56,13 +84,21 @@ header {
.fortschritt { margin: 0; font-size: 0.9rem; }
.dezent { color: var(--dezent); font-size: 0.85rem; }
ul { list-style: none; margin: 0.3rem 0 0; padding: 0; }
li {
border-top: 1px solid var(--rand);
padding: 0.35rem 0;
li { border-top: 1px solid var(--rand); }
.eintrag {
width: 100%;
border: none;
background: none;
border-radius: 6px;
text-align: left;
padding: 0.4rem 0.4rem;
margin: 0.1rem -0.4rem;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.eintrag:hover { background: var(--bg); }
.eintrag:focus-visible { outline: 2px solid var(--akzent); outline-offset: -2px; }
.stelle {
font-size: 0.72rem;
text-transform: uppercase;

View File

@@ -79,7 +79,7 @@ async function entfernen() {
<aside @contextmenu.prevent="leistenMenue">
<header>
<p class="gruppe">Projekte</p>
<slot />
<span class="werkzeuge"><slot /></span>
</header>
<ul>
<li
@@ -131,13 +131,35 @@ aside {
display: flex;
flex-direction: column;
gap: 0.4rem;
min-height: 0; /* sonst wächst die Leiste über die Zeile hinaus */
overflow-y: auto; /* lange Projektlisten scrollen für sich */
overscroll-behavior: contain;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
margin-bottom: 0.2rem;
}
/* Werkzeuge rechts zusammen, nicht über die Breite verteilt */
.werkzeuge {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.1rem;
}
@media (max-width: 46rem) {
aside {
border-right: none;
border-bottom: 1px solid var(--rand);
padding: 0.6rem 1rem;
max-height: 40vh;
}
ul { display: flex; flex-wrap: wrap; gap: 0.3rem; }
li { padding: 0.25rem 0.7rem; background: var(--bg); }
.name { flex: none; }
}
.gruppe {
font-size: 0.72rem;
font-weight: 600;

View File

@@ -12,4 +12,9 @@ export function schliesseMenue() {
kontextMenue.value = null
}
window.addEventListener('click', schliesseMenue)
// Capture-Phase: Elemente wie der Vorschlags-Hinweis stoppen das Bubbling mit
// @click.stop — in der Bubble-Phase bliebe das Menü dann offen stehen.
window.addEventListener('click', (e) => {
if (e.target.closest?.('.kontextmenue')) return // Auswahl darf zuerst laufen
schliesseMenue()
}, true)

View File

@@ -39,6 +39,36 @@ export function vorschlagsArt(p) {
return 'anpassen'
}
// Längste gemeinsame Teilfolge — die Grundlage für „was bleibt, was ändert sich"
function _gemeinsam(a, b) {
const t = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0))
for (let i = a.length - 1; i >= 0; i--) {
for (let j = b.length - 1; j >= 0; j--) {
t[i][j] = a[i] === b[j] ? t[i + 1][j + 1] + 1 : Math.max(t[i + 1][j], t[i][j + 1])
}
}
return t
}
// Der Vorschlag als Gegenüberstellung: was weg ist, was neu ist, was bleibt.
// Nur so sieht man vor dem Klick, was das ✓ wirklich tut.
export function vergleich(p) {
const alt = (p.original || '').split('\n').map(z => z.trim()).filter(Boolean)
const neu = vorschlagsZeilen(p)
const t = _gemeinsam(alt, neu)
const raus = []
let i = 0
let j = 0
while (i < alt.length && j < neu.length) {
if (alt[i] === neu[j]) raus.push({ art: 'gleich', text: alt[i++] }), j++
else if (t[i + 1][j] >= t[i][j + 1]) raus.push({ art: 'weg', text: alt[i++] })
else raus.push({ art: 'neu', text: neu[j++] })
}
while (i < alt.length) raus.push({ art: 'weg', text: alt[i++] })
while (j < neu.length) raus.push({ art: 'neu', text: neu[j++] })
return raus
}
// Neue Probleme werden angehängt, nicht ersetzt. Der Server kennt nur die Namen von
// vor dem Lauf; würde die Liste jede Sekunde neu gesetzt, verlöre benenneUm() seine Wirkung.
function uebernehmen(stand) {