diff --git a/README.md b/README.md index 250a31b..f285481 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Rein manuelle Pflege: Sidebar links = Projekte, Hauptbereich = Feature-Cards. Markdown-Dateien unter `daten//.md` — Projekt = Ordner, die Datei ist die Wahrheit. Der Editor bearbeitet immer den rohen Text; geparst wird nur für die Anzeige. -Format (Marker allein auf einer Zeile; `` ist wiederholbar, jede -`` gehört zur davorstehenden Funktion; Trennzeilen aus `---` sind Optik): +Format (Marker allein auf einer Zeile; `` ist wiederholbar, `` +und `` gehören zur davorstehenden Funktion; Trennzeilen aus `---` sind Optik): ```markdown @@ -26,14 +26,16 @@ Kurzer Satz, was das Ziel ist. Anmeldung mit E-Mail Passwort nie loggen + +- Formular öffnen +- Daten eingeben Abmeldung - -Notizen. ``` -Text vor dem ersten Marker, Inhalt unbekannter Marker und Regeln ohne Funktion erscheinen -in der Anzeige unter „Sonstiges"; die Datei bleibt unangetastet. Bearbeitet wird live auf +Funktionen erscheinen als Titelliste; Regeln und Flow zeigt die Karte erst beim Hover über +die Funktion. Text vor dem ersten Marker, Inhalt unbekannter Marker und verwaiste +regel/flow-Blöcke werden nicht angezeigt, aber beim Speichern erhalten. Bearbeitet wird live auf der Karte (Klick ins Feld, Verlassen speichert, Escape verwirft); jedes Speichern setzt die Datei neu zusammen. Titeländerungen benennen die Datei passend um. diff --git a/backend/parser.py b/backend/parser.py index 71f1b08..59058e8 100644 --- a/backend/parser.py +++ b/backend/parser.py @@ -1,8 +1,9 @@ """Zerlegt den rohen Feature-Text in Sektionen — nur für die Anzeige. -Format: titel (eine Zeile), ziel, dann wiederholbare -Blöcke, -denen -Blöcke zugeordnet werden, zuletzt sonstiges. -Präambel, unbekannte Marker und Regeln ohne Funktion landen in `sonstiges`. +Format: titel (eine Zeile), art, ziel, dann wiederholbare -Blöcke; +- und -Blöcke gehören zur davorstehenden Funktion. +Präambel, unbekannte Marker und verwaiste regel/flow-Blöcke landen in `sonstiges` +(wird nicht angezeigt, aber beim Speichern erhalten). Trennzeilen aus Bindestrichen (---) sind reine Optik und werden ignoriert. """ @@ -46,27 +47,24 @@ def zerlege(roh: str) -> dict: elif schluessel == "ziel": ziel.append(text) elif schluessel == "funktion": - funktionen.append({"text": text, "regeln": []}) + funktionen.append({"text": text, "regeln": [], "flow": []}) elif schluessel == "regel" and funktionen: funktionen[-1]["regeln"].append(text) - else: # Präambel, unbekannter Marker, regel ohne funktion + elif schluessel == "flow" and funktionen: + funktionen[-1]["flow"].append(text) + else: # Präambel, unbekannter Marker, regel/flow ohne funktion sonstiges.append(text) + for f in funktionen: + f["flow"] = "\n".join(f["flow"]) + # Titel ist eine Zeile: erste nicht-leere titel_zeilen = [z.strip() for t in titel for z in t.splitlines() if z.strip()] - # Funktionen-Bereich mit Markern rekonstruiert — für das Editor-Feld - teile = [] - for f in funktionen: - teile.append(f"\n{f['text']}") - for r in f["regeln"]: - teile.append(f"\n{r}") - return { "titel": titel_zeilen[0] if titel_zeilen else "", "art": "optional" if art and art[0].strip().lower() == "optional" else "kern", "ziel": "\n\n".join(ziel), "funktionen": funktionen, - "funktionen_roh": "\n".join(teile), "sonstiges": "\n\n".join(sonstiges), } diff --git a/backend/tests/test_parser.py b/backend/tests/test_parser.py index a8de7e5..fe8fcaa 100644 --- a/backend/tests/test_parser.py +++ b/backend/tests/test_parser.py @@ -3,6 +3,9 @@ from parser import zerlege VOLL = """ Login ---------------------------------------- + +kern +---------------------------------------- Nutzer können sich anmelden. ---------------------------------------- @@ -12,33 +15,32 @@ Anmeldung mit E-Mail Passwort nie loggen Sperre nach 5 Fehlversuchen + +- Formular öffnen +- Daten eingeben Abmeldung ----------------------------------------- - -Offen: 2FA. """ def test_alle_sektionen(): s = zerlege(VOLL) assert s["titel"] == "Login" + assert s["art"] == "kern" assert s["ziel"] == "Nutzer können sich anmelden." assert s["funktionen"] == [ - {"text": "Anmeldung mit E-Mail", "regeln": ["Passwort nie loggen", "Sperre nach 5 Fehlversuchen"]}, - {"text": "Abmeldung", "regeln": []}, + { + "text": "Anmeldung mit E-Mail", + "regeln": ["Passwort nie loggen", "Sperre nach 5 Fehlversuchen"], + "flow": "- Formular öffnen\n- Daten eingeben", + }, + {"text": "Abmeldung", "regeln": [], "flow": ""}, ] - assert s["sonstiges"] == "Offen: 2FA." -def test_funktionen_roh_rekonstruiert_marker(): - s = zerlege(VOLL) - assert s["funktionen_roh"] == ( - "\nAnmeldung mit E-Mail\n" - "\nPasswort nie loggen\n" - "\nSperre nach 5 Fehlversuchen\n" - "\nAbmeldung" - ) +def test_mehrere_flow_bloecke_werden_verbunden(): + s = zerlege("\nX\n\n- a\n\n- b\n") + assert s["funktionen"][0]["flow"] == "- a\n- b" def test_trenner_zeilen_werden_ignoriert(): @@ -57,10 +59,10 @@ def test_unbekannter_marker_landet_in_sonstiges(): assert s["sonstiges"] == "Geheim" -def test_regel_ohne_funktion_landet_in_sonstiges(): - s = zerlege("\nverwaist\n") +def test_regel_und_flow_ohne_funktion_landen_in_sonstiges(): + s = zerlege("\nverwaist\n\n- auch\n") assert s["funktionen"] == [] - assert s["sonstiges"] == "verwaist" + assert s["sonstiges"] == "verwaist\n\n- auch" def test_doppelter_marker_wird_konkateniert(): @@ -70,7 +72,7 @@ def test_doppelter_marker_wird_konkateniert(): def test_leerer_text(): s = zerlege("") - assert s == {"titel": "", "art": "kern", "ziel": "", "funktionen": [], "funktionen_roh": "", "sonstiges": ""} + assert s == {"titel": "", "art": "kern", "ziel": "", "funktionen": [], "sonstiges": ""} def test_art_optional(): diff --git a/daten/Planer/darkmode.md b/daten/Planer/darkmode.md new file mode 100644 index 0000000..d26c3c2 --- /dev/null +++ b/daten/Planer/darkmode.md @@ -0,0 +1,19 @@ + +Darkmode +---------------------------------------- + +optional +---------------------------------------- + + +---------------------------------------- + +Darkmode umschalten + +Icon: ☾ / ☀ + +Folgt beim ersten Start der System-Einstellung + +Zustand bleibt gespeichert + +- Klick auf Icon in der Sidebar oben diff --git a/daten/Planer/features.md b/daten/Planer/features.md new file mode 100644 index 0000000..84a5df7 --- /dev/null +++ b/daten/Planer/features.md @@ -0,0 +1,78 @@ + +Features +---------------------------------------- + +kern +---------------------------------------- + + +---------------------------------------- + +Ausführliche Ansicht umschalten + +Volle Breite + +Funktionen als Boxen in zwei Spalten + +Alle Details offen + +- Klick auf freie Kartenfläche +- Nochmal klicken schaltet zurück + +Feature als Datei speichern + +Projekt = Ordner unter daten/ + +Feature = Markdown-Datei im Projektordner + +Titeländerung benennt die Datei um + +Versionierbar per git + +Feature erstellen + +Icon: + + +Titel direkt zur Bearbeitung geöffnet + +Karten alphabetisch nach Titel sortiert + +- Klick auf leere Card +- Card wird erstellt + +Features entfernen + +Icon: x + +- Hover über Card +- Auf Icon klicken +- Icon wird zum Bestätigungs Icon +- Klick zum entfernen + +Kern/Optional umschalten + +Kern blau umrandet + +Optional grün umrandet + +- Klick auf Badge + +Titel anpassen + +Enter schließt ab + +Verlassen speichert + +Escape verwirft + +- Hover über Card +- Klick auf Titel +- Inhalt ändern + +Ziel anpassen + +Leer: Platzhalter „Ziel ergänzen" klicken + +- Hover über Card +- Klick auf Zieltext +- Inhalt anpassen diff --git a/daten/Planer/funktionen.md b/daten/Planer/funktionen.md new file mode 100644 index 0000000..6cd4bfe --- /dev/null +++ b/daten/Planer/funktionen.md @@ -0,0 +1,59 @@ + +Funktionen +---------------------------------------- + +kern +---------------------------------------- + + +---------------------------------------- + +Flow anpassen + +Ein Flow je Funktion + +Leeren entfernt den Flow + +- Hover über Funktion +- Klick auf Flow oder + Flow +- Inhalt anpassen + +Funktion anpassen + +- Klick auf Text +- Inhalt ändern + +Funktion entfernen + +Feld leeren entfernt auch + +- Hover über Funktion +- Klick auf x + +Funktion hinzufügen + +Alphabetisch sortiert + +Neue Funktion rückt nach dem Speichern an ihre Position + +- Hover über Card +- Klick auf + Funktion +- Inhalt eingeben + +Regel anpassen + +- Klick auf Text +- Inhalt ändern + +Regel entfernen + +Feld leeren entfernt auch + +- Hover über Regel +- Klick auf x + +Regel hinzufügen + +- Hover über Funktion +- Klick auf + Regel +- Inhalt eingeben diff --git a/daten/Planer/projekte.md b/daten/Planer/projekte.md new file mode 100644 index 0000000..f9288a8 --- /dev/null +++ b/daten/Planer/projekte.md @@ -0,0 +1,47 @@ + +Projekte +---------------------------------------- + +kern +---------------------------------------- + + +---------------------------------------- + +Projekt anpassen + +Icon: Stift + +Enter bestätigt + +Escape bricht ab + +- Hover über Projektname +- Klick auf Icon +- Inhalt anpassen + +Projekt auswählen + +Aktives Projekt ist hervorgehoben + +- Klick auf Name +- Features werden im Hauptbereich angezeigt + +Projekt entfernen + +Icon: x + +- Hover über Name +- Klick auf Icon +- Icon wird zum Bestätigungs Icon +- Klick zum entfernen + +Projekt erstellen + +Alphabetisch in der Sidebar sortiert + +Feature-Anzahl neben dem Namen + +- Inputfeld titel eingeben +- Mit Icon oder Enter erstellen +- Projekt wird direkt ausgewählt diff --git a/daten/Planer2/neues-feature-2.md b/daten/Planer2/neues-feature-2.md deleted file mode 100644 index a3e8638..0000000 --- a/daten/Planer2/neues-feature-2.md +++ /dev/null @@ -1,11 +0,0 @@ - -Neues Feature ----------------------------------------- - -kern ----------------------------------------- - -adsda ----------------------------------------- - - diff --git a/frontend/src/components/FeatureKarte.vue b/frontend/src/components/FeatureKarte.vue index 093d88b..7b23163 100644 --- a/frontend/src/components/FeatureKarte.vue +++ b/frontend/src/components/FeatureKarte.vue @@ -15,8 +15,10 @@ function kopie(sekt) { titel: sekt.titel, art: sekt.art, ziel: sekt.ziel, - funktionen: sekt.funktionen.map(f => ({ text: f.text, regeln: [...f.regeln] })), - sonstiges: sekt.sonstiges, + funktionen: sekt.funktionen + .map(f => ({ text: f.text, regeln: [...f.regeln], flow: f.flow })) + .sort((a, b) => a.text.localeCompare(b.text, 'de', { sensitivity: 'base' })), + sonstiges: sekt.sonstiges, // nicht angezeigt, aber beim Speichern erhalten } } @@ -39,8 +41,8 @@ function istAktiv(feld, i, j) { function wert(feld, i, j) { if (feld === 'titel') return s.value.titel if (feld === 'ziel') return s.value.ziel - if (feld === 'sonstiges') return s.value.sonstiges if (feld === 'funktion') return s.value.funktionen[i].text + if (feld === 'flow') return s.value.funktionen[i].flow return s.value.funktionen[i].regeln[j] } @@ -77,11 +79,11 @@ async function fertig() { s.value.titel = neu } else if (a.feld === 'ziel') { s.value.ziel = neu - } else if (a.feld === 'sonstiges') { - s.value.sonstiges = neu } else if (a.feld === 'funktion') { if (!neu) s.value.funktionen.splice(a.i, 1) // leeren = löschen else s.value.funktionen[a.i].text = neu + } else if (a.feld === 'flow') { + s.value.funktionen[a.i].flow = neu } else if (a.feld === 'regel') { if (!neu) s.value.funktionen[a.i].regeln.splice(a.j, 1) else s.value.funktionen[a.i].regeln[a.j] = neu @@ -96,8 +98,8 @@ function abbrechenLeer(a) { } function neueFunktion() { - s.value.funktionen.push({ text: '', regeln: [] }) - starte('funktion', s.value.funktionen.length - 1) + s.value.funktionen.unshift({ text: '', regeln: [], flow: '' }) + starte('funktion', 0) } function neueRegel(i) { @@ -130,10 +132,14 @@ function baueRoh() { `\n${s.value.ziel}`, ] const funktionen = s.value.funktionen - .map(f => [`\n${f.text}`, ...f.regeln.map(r => `\n${r}`)].join('\n')) + .map(f => { + const bloecke = [`\n${f.text}`, ...f.regeln.map(r => `\n${r}`)] + if (f.flow) bloecke.push(`\n${f.flow}`) + return bloecke.join('\n') + }) .join('\n') if (funktionen) teile.push(funktionen) - teile.push(`\n${s.value.sonstiges}`) + if (s.value.sonstiges) teile.push(`\n${s.value.sonstiges}`) return teile.join(`\n${TRENNER}\n`) + '\n' } @@ -147,8 +153,15 @@ async function speichern() { } } +const loeschBestaetigung = ref(false) + async function loeschen() { - if (!confirm(`Feature „${s.value.titel}“ löschen?`)) return + // Erster Klick: Icon wird zum Bestätigungs-Icon, zweiter Klick löscht + if (!loeschBestaetigung.value) { + loeschBestaetigung.value = true + return + } + loeschBestaetigung.value = false try { await featureLoeschen(props.projekt, props.feature.id) emit('geloescht') @@ -161,13 +174,31 @@ function zeilen(text) { return Math.max(2, text.split('\n').length) } +// Flow-Text („- Schritt“ je Zeile) für die nummerierte Anzeige zerlegen +function flowSchritte(flow) { + return flow.split('\n').map(z => z.replace(/^[-*]\s*/, '').trim()).filter(Boolean) +} + +// Ausführliche Ansicht: Klick auf freie Kartenfläche schaltet um +const gross = ref(false) + +function wechsleAnsicht(e) { + if (e.target.closest('button, input, textarea, .klickbar, h3')) return + gross.value = !gross.value +} + onMounted(() => { if (props.titelBearbeiten) starte('titel') })