update
This commit is contained in:
20
README.md
20
README.md
@@ -12,8 +12,11 @@ Rein manuelle Pflege: Sidebar links = Projekte, Hauptbereich = Feature-Cards.
|
|||||||
Markdown-Dateien unter `daten/<projekt>/<feature>.md` — Projekt = Ordner, die Datei ist
|
Markdown-Dateien unter `daten/<projekt>/<feature>.md` — Projekt = Ordner, die Datei ist
|
||||||
die Wahrheit. Der Editor bearbeitet immer den rohen Text; geparst wird nur für die Anzeige.
|
die Wahrheit. Der Editor bearbeitet immer den rohen Text; geparst wird nur für die Anzeige.
|
||||||
|
|
||||||
Format (Marker allein auf einer Zeile; `<!-- funktion -->` ist wiederholbar, `<!-- regel -->`
|
Format (Marker allein auf einer Zeile; `<!-- funktion -->` ist wiederholbar; `<!-- ziel -->`,
|
||||||
und `<!-- flow -->` gehören zur davorstehenden Funktion; Trennzeilen aus `---` sind Optik):
|
`<!-- regeln -->` und `<!-- testen -->` nach einer Funktion gehören zu ihr — `ziel`/`regeln`
|
||||||
|
vor der ersten Funktion gehören zur Karte; regeln/testen: je eine `- `-Zeile pro Eintrag;
|
||||||
|
`<!-- regel -->`-Blöcke aus dem Alt-Format werden zu Zeilen zusammengeführt;
|
||||||
|
Trennzeilen aus `---` sind Optik):
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
<!-- titel -->
|
<!-- titel -->
|
||||||
@@ -24,18 +27,19 @@ kern
|
|||||||
Kurzer Satz, was das Ziel ist.
|
Kurzer Satz, was das Ziel ist.
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Anmeldung mit E-Mail
|
Anmeldung mit E-Mail
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Passwort nie loggen
|
- Passwort nie loggen
|
||||||
<!-- flow -->
|
<!-- testen -->
|
||||||
- Formular öffnen
|
- Formular öffnen
|
||||||
- Daten eingeben
|
- Daten eingeben
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Abmeldung
|
Abmeldung
|
||||||
```
|
```
|
||||||
|
|
||||||
Funktionen erscheinen als Titelliste; Regeln und Flow zeigt die Karte erst beim Hover über
|
Funktionen erscheinen als Titelliste; Regeln und Testen zeigt die ausführliche Ansicht.
|
||||||
die Funktion. Text vor dem ersten Marker, Inhalt unbekannter Marker und verwaiste
|
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
|
regeln/testen-Blöcke (auch Alt-Marker `flow`) werden nicht angezeigt, aber beim Speichern
|
||||||
|
erhalten. Bearbeitet wird live auf
|
||||||
der Karte (Klick ins Feld, Verlassen speichert, Escape verwirft); jedes Speichern setzt
|
der Karte (Klick ins Feld, Verlassen speichert, Escape verwirft); jedes Speichern setzt
|
||||||
die Datei neu zusammen. Titeländerungen benennen die Datei passend um.
|
die Datei neu zusammen. Titeländerungen benennen die Datei passend um.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Zerlegt den rohen Feature-Text in Sektionen — nur für die Anzeige.
|
"""Zerlegt den rohen Feature-Text in Sektionen — nur für die Anzeige.
|
||||||
|
|
||||||
Format: titel (eine Zeile), art, ziel, dann wiederholbare <!-- funktion -->-Blöcke;
|
Format: titel (eine Zeile), art, ziel, dann wiederholbare <!-- funktion -->-Blöcke;
|
||||||
<!-- regel -->- und <!-- flow -->-Blöcke gehören zur davorstehenden Funktion.
|
<!-- regeln -->- und <!-- testen -->-Blöcke (je eine `- `-Zeile pro Eintrag) gehören zur
|
||||||
Präambel, unbekannte Marker und verwaiste regel/flow-Blöcke landen in `sonstiges`
|
davorstehenden Funktion. Alt-Format wird weiter gelesen: <!-- regel -->-Blöcke (einer je
|
||||||
(wird nicht angezeigt, aber beim Speichern erhalten).
|
Regel) und <!-- flow --> (heute testen) werden zusammengeführt. Präambel, unbekannte
|
||||||
|
Marker und verwaiste Blöcke landen in `sonstiges` (wird nicht angezeigt, aber beim
|
||||||
|
Speichern erhalten).
|
||||||
Trennzeilen aus Bindestrichen (---) sind reine Optik und werden ignoriert.
|
Trennzeilen aus Bindestrichen (---) sind reine Optik und werden ignoriert.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ def zerlege(roh: str) -> dict:
|
|||||||
|
|
||||||
titel = []
|
titel = []
|
||||||
ziel = []
|
ziel = []
|
||||||
|
regeln = []
|
||||||
funktionen = []
|
funktionen = []
|
||||||
sonstiges = []
|
sonstiges = []
|
||||||
art = []
|
art = []
|
||||||
@@ -45,18 +48,22 @@ def zerlege(roh: str) -> dict:
|
|||||||
elif schluessel == "art":
|
elif schluessel == "art":
|
||||||
art.append(text)
|
art.append(text)
|
||||||
elif schluessel == "ziel":
|
elif schluessel == "ziel":
|
||||||
ziel.append(text)
|
# vor der ersten Funktion: Karten-Ziel, danach: Ziel der Funktion
|
||||||
|
(funktionen[-1]["ziel"] if funktionen else ziel).append(text)
|
||||||
elif schluessel == "funktion":
|
elif schluessel == "funktion":
|
||||||
funktionen.append({"text": text, "regeln": [], "flow": []})
|
funktionen.append({"text": text, "ziel": [], "regeln": [], "testen": []})
|
||||||
elif schluessel == "regel" and funktionen:
|
elif schluessel in ("regel", "regeln"):
|
||||||
funktionen[-1]["regeln"].append(text)
|
# vor der ersten Funktion: Karten-Regeln, danach: Regeln der Funktion
|
||||||
elif schluessel == "flow" and funktionen:
|
(funktionen[-1]["regeln"] if funktionen else regeln).append(text)
|
||||||
funktionen[-1]["flow"].append(text)
|
elif schluessel in ("flow", "testen") and funktionen:
|
||||||
else: # Präambel, unbekannter Marker, regel/flow ohne funktion
|
funktionen[-1]["testen"].append(text)
|
||||||
|
else: # Präambel, unbekannter Marker, regeln/testen ohne funktion
|
||||||
sonstiges.append(text)
|
sonstiges.append(text)
|
||||||
|
|
||||||
for f in funktionen:
|
for f in funktionen:
|
||||||
f["flow"] = "\n".join(f["flow"])
|
f["ziel"] = "\n\n".join(f["ziel"])
|
||||||
|
f["regeln"] = "\n".join(f["regeln"])
|
||||||
|
f["testen"] = "\n".join(f["testen"])
|
||||||
|
|
||||||
# Titel ist eine Zeile: erste nicht-leere
|
# Titel ist eine Zeile: erste nicht-leere
|
||||||
titel_zeilen = [z.strip() for t in titel for z in t.splitlines() if z.strip()]
|
titel_zeilen = [z.strip() for t in titel for z in t.splitlines() if z.strip()]
|
||||||
@@ -65,6 +72,7 @@ def zerlege(roh: str) -> dict:
|
|||||||
"titel": titel_zeilen[0] if titel_zeilen else "",
|
"titel": titel_zeilen[0] if titel_zeilen else "",
|
||||||
"art": "optional" if art and art[0].strip().lower() == "optional" else "kern",
|
"art": "optional" if art and art[0].strip().lower() == "optional" else "kern",
|
||||||
"ziel": "\n\n".join(ziel),
|
"ziel": "\n\n".join(ziel),
|
||||||
|
"regeln": "\n".join(regeln),
|
||||||
"funktionen": funktionen,
|
"funktionen": funktionen,
|
||||||
"sonstiges": "\n\n".join(sonstiges),
|
"sonstiges": "\n\n".join(sonstiges),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ Nutzer können sich anmelden.
|
|||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Anmeldung mit E-Mail
|
Anmeldung mit E-Mail
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Passwort nie loggen
|
- Passwort nie loggen
|
||||||
<!-- regel -->
|
- Sperre nach 5 Fehlversuchen
|
||||||
Sperre nach 5 Fehlversuchen
|
<!-- testen -->
|
||||||
<!-- flow -->
|
|
||||||
- Formular öffnen
|
- Formular öffnen
|
||||||
- Daten eingeben
|
- Daten eingeben
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
@@ -31,16 +30,33 @@ def test_alle_sektionen():
|
|||||||
assert s["funktionen"] == [
|
assert s["funktionen"] == [
|
||||||
{
|
{
|
||||||
"text": "Anmeldung mit E-Mail",
|
"text": "Anmeldung mit E-Mail",
|
||||||
"regeln": ["Passwort nie loggen", "Sperre nach 5 Fehlversuchen"],
|
"ziel": "",
|
||||||
"flow": "- Formular öffnen\n- Daten eingeben",
|
"regeln": "- Passwort nie loggen\n- Sperre nach 5 Fehlversuchen",
|
||||||
|
"testen": "- Formular öffnen\n- Daten eingeben",
|
||||||
},
|
},
|
||||||
{"text": "Abmeldung", "regeln": [], "flow": ""},
|
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_mehrere_flow_bloecke_werden_verbunden():
|
def test_ziel_nach_funktion_gehoert_zur_funktion():
|
||||||
s = zerlege("<!-- funktion -->\nX\n<!-- flow -->\n- a\n<!-- flow -->\n- b\n")
|
s = zerlege("<!-- ziel -->\nKarte\n<!-- funktion -->\nX\n<!-- ziel -->\nFunktion\n")
|
||||||
assert s["funktionen"][0]["flow"] == "- a\n- b"
|
assert s["ziel"] == "Karte"
|
||||||
|
assert s["funktionen"][0]["ziel"] == "Funktion"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mehrere_testen_bloecke_werden_verbunden():
|
||||||
|
s = zerlege("<!-- funktion -->\nX\n<!-- testen -->\n- a\n<!-- testen -->\n- b\n")
|
||||||
|
assert s["funktionen"][0]["testen"] == "- a\n- b"
|
||||||
|
|
||||||
|
|
||||||
|
def test_alter_flow_marker_wird_gelesen():
|
||||||
|
s = zerlege("<!-- funktion -->\nX\n<!-- flow -->\n- a\n")
|
||||||
|
assert s["funktionen"][0]["testen"] == "- a"
|
||||||
|
|
||||||
|
|
||||||
|
def test_alte_regel_bloecke_werden_zusammengefuehrt():
|
||||||
|
s = zerlege("<!-- funktion -->\nX\n<!-- regel -->\na\n<!-- regel -->\nb\n")
|
||||||
|
assert s["funktionen"][0]["regeln"] == "a\nb"
|
||||||
|
|
||||||
|
|
||||||
def test_trenner_zeilen_werden_ignoriert():
|
def test_trenner_zeilen_werden_ignoriert():
|
||||||
@@ -59,10 +75,16 @@ def test_unbekannter_marker_landet_in_sonstiges():
|
|||||||
assert s["sonstiges"] == "Geheim"
|
assert s["sonstiges"] == "Geheim"
|
||||||
|
|
||||||
|
|
||||||
def test_regel_und_flow_ohne_funktion_landen_in_sonstiges():
|
def test_regeln_vor_erster_funktion_gehoeren_zur_karte():
|
||||||
s = zerlege("<!-- regel -->\nverwaist\n<!-- flow -->\n- auch\n")
|
s = zerlege("<!-- regeln -->\n- karte\n<!-- funktion -->\nX\n<!-- regeln -->\n- funktion\n")
|
||||||
|
assert s["regeln"] == "- karte"
|
||||||
|
assert s["funktionen"][0]["regeln"] == "- funktion"
|
||||||
|
|
||||||
|
|
||||||
|
def test_testen_ohne_funktion_landet_in_sonstiges():
|
||||||
|
s = zerlege("<!-- testen -->\n- verwaist\n")
|
||||||
assert s["funktionen"] == []
|
assert s["funktionen"] == []
|
||||||
assert s["sonstiges"] == "verwaist\n\n- auch"
|
assert s["sonstiges"] == "- verwaist"
|
||||||
|
|
||||||
|
|
||||||
def test_doppelter_marker_wird_konkateniert():
|
def test_doppelter_marker_wird_konkateniert():
|
||||||
@@ -72,7 +94,7 @@ def test_doppelter_marker_wird_konkateniert():
|
|||||||
|
|
||||||
def test_leerer_text():
|
def test_leerer_text():
|
||||||
s = zerlege("")
|
s = zerlege("")
|
||||||
assert s == {"titel": "", "art": "kern", "ziel": "", "funktionen": [], "sonstiges": ""}
|
assert s == {"titel": "", "art": "kern", "ziel": "", "regeln": "", "funktionen": [], "sonstiges": ""}
|
||||||
|
|
||||||
|
|
||||||
def test_art_optional():
|
def test_art_optional():
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ optional
|
|||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Darkmode umschalten
|
Darkmode umschalten
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Icon: ☾ / ☀
|
- Icon: ☾ / ☀
|
||||||
<!-- regel -->
|
- Folgt beim ersten Start der System-Einstellung
|
||||||
Folgt beim ersten Start der System-Einstellung
|
- Zustand bleibt gespeichert
|
||||||
<!-- regel -->
|
<!-- testen -->
|
||||||
Zustand bleibt gespeichert
|
|
||||||
<!-- flow -->
|
|
||||||
- Klick auf Icon in der Sidebar oben
|
- Klick auf Icon in der Sidebar oben
|
||||||
|
|||||||
@@ -5,74 +5,85 @@ Features
|
|||||||
kern
|
kern
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- ziel -->
|
<!-- ziel -->
|
||||||
|
Gruppieren Informationen zu ähnlichen Funktionen
|
||||||
|
----------------------------------------
|
||||||
|
<!-- regeln -->
|
||||||
|
- Alphabetisch sortiert
|
||||||
|
- Unter daten/ als md Datei gespeichert
|
||||||
|
- Verlassen speichert
|
||||||
|
- Escape verwirft
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Ausführliche Ansicht umschalten
|
Anzeigemodus umschalten
|
||||||
<!-- regel -->
|
<!-- ziel -->
|
||||||
Volle Breite
|
Kompaktmodus zum lesen und Vollmodus zum vertiefen und anpassen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Funktionen als Boxen in zwei Spalten
|
- Standard ist Kompaktmodus
|
||||||
<!-- regel -->
|
- Vollmodus geht über responsive Spalten
|
||||||
Alle Details offen
|
- Vollmodus zeigt alles
|
||||||
<!-- flow -->
|
- Kompaktmodus zeigt Ziel und Funktionstitel als Liste
|
||||||
|
<!-- testen -->
|
||||||
|
- Klick auf Karte
|
||||||
|
- Vollmodus sichtbar
|
||||||
- Klick auf freie Kartenfläche
|
- Klick auf freie Kartenfläche
|
||||||
- Nochmal klicken schaltet zurück
|
- Kompaktmodus sichtbar
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Feature als Datei speichern
|
Badge umschalten
|
||||||
<!-- regel -->
|
<!-- ziel -->
|
||||||
Projekt = Ordner unter daten/
|
Unterscheidet die Kern- und Optionalen Features
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Feature = Markdown-Datei im Projektordner
|
- Kernfarbe blau
|
||||||
<!-- regel -->
|
- Optionalfarbe grün
|
||||||
Titeländerung benennt die Datei um
|
- Verändert die Randfarbe vom Feature
|
||||||
<!-- regel -->
|
<!-- testen -->
|
||||||
Versionierbar per git
|
- Vollmodus öffnen
|
||||||
|
- Klick auf Badge zum toggeln
|
||||||
|
<!-- funktion -->
|
||||||
|
Feature entfernen
|
||||||
|
<!-- regeln -->
|
||||||
|
- x-Icon
|
||||||
|
<!-- testen -->
|
||||||
|
- RMB auf Featurecard
|
||||||
|
- Klick auf "Feature entfernen"
|
||||||
|
- Bestätigen im Modal
|
||||||
|
- Feature entfernt
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Feature erstellen
|
Feature erstellen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Icon: +
|
- +-Icon
|
||||||
<!-- regel -->
|
- Feature direkt in Vollmodus geöffnet
|
||||||
Titel direkt zur Bearbeitung geöffnet
|
<!-- testen -->
|
||||||
<!-- regel -->
|
- RMB auf Hauptbereich
|
||||||
Karten alphabetisch nach Titel sortiert
|
- Klick auf "Feature erstellen"
|
||||||
<!-- flow -->
|
- Im Modal Namen eingeben
|
||||||
- Klick auf leere Card
|
- Bestätigen
|
||||||
- Card wird erstellt
|
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Features entfernen
|
Regeln anpassen
|
||||||
<!-- regel -->
|
<!-- ziel -->
|
||||||
Icon: x
|
Spezifische Hinweise für Features
|
||||||
<!-- flow -->
|
<!-- regeln -->
|
||||||
- Hover über Card
|
- Nur paar Wörter je Regel
|
||||||
- Auf Icon klicken
|
<!-- testen -->
|
||||||
- Icon wird zum Bestätigungs Icon
|
- Vollmodus öffnen
|
||||||
- Klick zum entfernen
|
- Klick auf Regeln
|
||||||
<!-- funktion -->
|
- Inhalt anpassen
|
||||||
Kern/Optional umschalten
|
|
||||||
<!-- regel -->
|
|
||||||
Kern blau umrandet
|
|
||||||
<!-- regel -->
|
|
||||||
Optional grün umrandet
|
|
||||||
<!-- flow -->
|
|
||||||
- Klick auf Badge
|
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Titel anpassen
|
Titel anpassen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Enter schließt ab
|
- 1-2 Wörter
|
||||||
<!-- regel -->
|
- Aktualisiert den Dateinamen
|
||||||
Verlassen speichert
|
<!-- testen -->
|
||||||
<!-- regel -->
|
- Vollmodus öffnen
|
||||||
Escape verwirft
|
|
||||||
<!-- flow -->
|
|
||||||
- Hover über Card
|
|
||||||
- Klick auf Titel
|
- Klick auf Titel
|
||||||
- Inhalt ändern
|
- Inhalt ändern
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Ziel anpassen
|
Ziel anpassen
|
||||||
<!-- regel -->
|
<!-- ziel -->
|
||||||
Leer: Platzhalter „Ziel ergänzen" klicken
|
Beschreibt kurz das Kernziel vom Feature
|
||||||
<!-- flow -->
|
<!-- regeln -->
|
||||||
- Hover über Card
|
- 1-2 simple Sätze
|
||||||
- Klick auf Zieltext
|
- Nur wenn der Titel nicht selbsterklärend ist
|
||||||
|
<!-- testen -->
|
||||||
|
- Vollmodus öffnen
|
||||||
|
- Klick auf Ziel
|
||||||
- Inhalt anpassen
|
- Inhalt anpassen
|
||||||
|
|||||||
@@ -8,52 +8,46 @@ kern
|
|||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Flow anpassen
|
Testen anpassen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Ein Flow je Funktion
|
- Ein Testen-Block je Funktion
|
||||||
<!-- regel -->
|
- Leeren entfernt den Block
|
||||||
Leeren entfernt den Flow
|
<!-- testen -->
|
||||||
<!-- flow -->
|
- Klick auf Testen oder + Testen
|
||||||
- Hover über Funktion
|
|
||||||
- Klick auf Flow oder + Flow
|
|
||||||
- Inhalt anpassen
|
- Inhalt anpassen
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Funktion anpassen
|
Funktion anpassen
|
||||||
<!-- flow -->
|
<!-- testen -->
|
||||||
- Klick auf Text
|
- Klick auf Text
|
||||||
- Inhalt ändern
|
- Inhalt ändern
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Funktion entfernen
|
Funktion entfernen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Feld leeren entfernt auch
|
- Feld leeren entfernt auch
|
||||||
<!-- flow -->
|
<!-- testen -->
|
||||||
- Hover über Funktion
|
- Hover über Funktion
|
||||||
- Klick auf x
|
- Klick auf x
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Funktion hinzufügen
|
Funktion hinzufügen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Alphabetisch sortiert
|
- Alphabetisch sortiert
|
||||||
<!-- regel -->
|
- Neue Funktion rückt nach dem Speichern an ihre Position
|
||||||
Neue Funktion rückt nach dem Speichern an ihre Position
|
<!-- testen -->
|
||||||
<!-- flow -->
|
|
||||||
- Hover über Card
|
|
||||||
- Klick auf + Funktion
|
- Klick auf + Funktion
|
||||||
- Inhalt eingeben
|
- Inhalt eingeben
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Regel anpassen
|
Ziel anpassen
|
||||||
<!-- flow -->
|
<!-- regeln -->
|
||||||
- Klick auf Text
|
- Ein Ziel je Funktion
|
||||||
- Inhalt ändern
|
- Leeren entfernt das Ziel
|
||||||
|
<!-- testen -->
|
||||||
|
- Klick auf Ziel oder + Ziel
|
||||||
|
- Inhalt anpassen
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Regel entfernen
|
Regeln anpassen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Feld leeren entfernt auch
|
- Eine Regel je Zeile
|
||||||
<!-- flow -->
|
- Leeren entfernt alle Regeln
|
||||||
- Hover über Regel
|
<!-- testen -->
|
||||||
- Klick auf x
|
- Klick auf Regeln oder + Regeln
|
||||||
<!-- funktion -->
|
- Inhalt anpassen
|
||||||
Regel hinzufügen
|
|
||||||
<!-- flow -->
|
|
||||||
- Hover über Funktion
|
|
||||||
- Klick auf + Regel
|
|
||||||
- Inhalt eingeben
|
|
||||||
|
|||||||
@@ -5,43 +5,45 @@ Projekte
|
|||||||
kern
|
kern
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- ziel -->
|
<!-- ziel -->
|
||||||
|
Gruppiert Features für ein Projekt
|
||||||
|
----------------------------------------
|
||||||
|
<!-- regeln -->
|
||||||
|
- Alphabetisch sortiert
|
||||||
|
- Aktives Projekt ist hervorgehoben
|
||||||
|
- 1-2 Wörter für den Namen
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Projekt anpassen
|
Projekt anpassen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Icon: Stift
|
- Stift-Icon
|
||||||
<!-- regel -->
|
<!-- testen -->
|
||||||
Enter bestätigt
|
- RMB auf Projektname
|
||||||
<!-- regel -->
|
- Klick auf "Projekt anpassen"
|
||||||
Escape bricht ab
|
|
||||||
<!-- flow -->
|
|
||||||
- Hover über Projektname
|
|
||||||
- Klick auf Icon
|
|
||||||
- Inhalt anpassen
|
- Inhalt anpassen
|
||||||
|
- Rausklicken
|
||||||
|
- Name ist angepasst
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Projekt auswählen
|
Projekt aufrufen
|
||||||
<!-- regel -->
|
<!-- testen -->
|
||||||
Aktives Projekt ist hervorgehoben
|
- LMB auf Projektname
|
||||||
<!-- flow -->
|
- Features werden angezeigt
|
||||||
- Klick auf Name
|
|
||||||
- Features werden im Hauptbereich angezeigt
|
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Projekt entfernen
|
Projekt entfernen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Icon: x
|
- X-Icon
|
||||||
<!-- flow -->
|
- Auch Features entfernen
|
||||||
- Hover über Name
|
<!-- testen -->
|
||||||
- Klick auf Icon
|
- RMB auf Projektname
|
||||||
- Icon wird zum Bestätigungs Icon
|
- Klick auf "Projekt entfernen"
|
||||||
- Klick zum entfernen
|
- Bestätigen im Modal
|
||||||
|
- Projekt entfernt
|
||||||
<!-- funktion -->
|
<!-- funktion -->
|
||||||
Projekt erstellen
|
Projekt erstellen
|
||||||
<!-- regel -->
|
<!-- regeln -->
|
||||||
Alphabetisch in der Sidebar sortiert
|
- Projekt wird direkt aufgerufen
|
||||||
<!-- regel -->
|
- +-Icon
|
||||||
Feature-Anzahl neben dem Namen
|
<!-- testen -->
|
||||||
<!-- flow -->
|
- RMB auf Sidebar
|
||||||
- Inputfeld titel eingeben
|
- Klick auf "Projekt erstellen"
|
||||||
- Mit Icon oder Enter erstellen
|
- Im Modal Namen eingeben
|
||||||
- Projekt wird direkt ausgewählt
|
- Bestätigen
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import { fetchProjekte } from './api.js'
|
import { fetchProjekte } from './api.js'
|
||||||
import SeitenLeiste from './components/SeitenLeiste.vue'
|
import SeitenLeiste from './components/SeitenLeiste.vue'
|
||||||
import FeatureListe from './components/FeatureListe.vue'
|
import FeatureListe from './components/FeatureListe.vue'
|
||||||
@@ -21,13 +21,25 @@ function umbenannt(alt, neu) {
|
|||||||
ladeProjekte()
|
ladeProjekte()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zuletzt aktives Projekt überlebt das Neuladen
|
||||||
|
watch(aktivesProjekt, name => {
|
||||||
|
if (name) localStorage.setItem('aktivesProjekt', name)
|
||||||
|
else localStorage.removeItem('aktivesProjekt')
|
||||||
|
})
|
||||||
|
|
||||||
function wechsleDarkMode() {
|
function wechsleDarkMode() {
|
||||||
dunkel.value = !dunkel.value
|
dunkel.value = !dunkel.value
|
||||||
document.documentElement.classList.toggle('dark', dunkel.value)
|
document.documentElement.classList.toggle('dark', dunkel.value)
|
||||||
localStorage.setItem('darkMode', dunkel.value)
|
localStorage.setItem('darkMode', dunkel.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(ladeProjekte)
|
onMounted(async () => {
|
||||||
|
await ladeProjekte()
|
||||||
|
const gemerkt = localStorage.getItem('aktivesProjekt')
|
||||||
|
if (gemerkt && projekte.value.some(p => p.name === gemerkt)) {
|
||||||
|
aktivesProjekt.value = gemerkt
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -88,6 +88,50 @@ input, textarea {
|
|||||||
|
|
||||||
.meldung { color: var(--rot); font-size: 0.85rem; }
|
.meldung { color: var(--rot); font-size: 0.85rem; }
|
||||||
|
|
||||||
|
.kontextmenue {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--rand);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
.kontextmenue button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.kontextmenue button:hover { background: var(--bg); }
|
||||||
|
|
||||||
|
.schleier {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
min-width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.7rem;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.modal h2 { margin: 0; font-size: 1rem; }
|
||||||
|
.modal .frage { margin: 0; }
|
||||||
|
.modal footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onMounted, ref, watch } from 'vue'
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import { featureSpeichern, featureLoeschen } from '../api.js'
|
import { featureSpeichern } from '../api.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
projekt: { type: String, required: true },
|
projekt: { type: String, required: true },
|
||||||
feature: { type: Object, required: true },
|
feature: { type: Object, required: true },
|
||||||
titelBearbeiten: { type: Boolean, default: false }, // frisch angelegt: Titel direkt öffnen
|
vollmodus: { type: Boolean, default: false }, // frisch angelegt: direkt im Vollmodus öffnen
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['aktualisiert', 'geloescht'])
|
const emit = defineEmits(['aktualisiert', 'kontextmenue'])
|
||||||
|
|
||||||
function kopie(sekt) {
|
function kopie(sekt) {
|
||||||
return {
|
return {
|
||||||
titel: sekt.titel,
|
titel: sekt.titel,
|
||||||
art: sekt.art,
|
art: sekt.art,
|
||||||
ziel: sekt.ziel,
|
ziel: sekt.ziel,
|
||||||
|
regeln: sekt.regeln,
|
||||||
funktionen: sekt.funktionen
|
funktionen: sekt.funktionen
|
||||||
.map(f => ({ text: f.text, regeln: [...f.regeln], flow: f.flow }))
|
.map(f => ({ text: f.text, ziel: f.ziel, regeln: f.regeln, testen: f.testen }))
|
||||||
.sort((a, b) => a.text.localeCompare(b.text, 'de', { sensitivity: 'base' })),
|
.sort((a, b) => a.text.localeCompare(b.text, 'de', { sensitivity: 'base' })),
|
||||||
sonstiges: sekt.sonstiges, // nicht angezeigt, aber beim Speichern erhalten
|
sonstiges: sekt.sonstiges, // nicht angezeigt, aber beim Speichern erhalten
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ const s = ref(kopie(props.feature.sektionen))
|
|||||||
watch(() => props.feature, f => { s.value = kopie(f.sektionen) })
|
watch(() => props.feature, f => { s.value = kopie(f.sektionen) })
|
||||||
|
|
||||||
const aktiv = ref(null) // {feld, i, j, original} — das eine gerade offene Eingabefeld
|
const aktiv = ref(null) // {feld, i, j, original} — das eine gerade offene Eingabefeld
|
||||||
|
const gross = ref(false) // ausführliche Ansicht
|
||||||
const entwurf = ref('')
|
const entwurf = ref('')
|
||||||
const meldung = ref('')
|
const meldung = ref('')
|
||||||
const feldRef = ref(null)
|
const feldRef = ref(null)
|
||||||
@@ -40,13 +42,14 @@ function istAktiv(feld, i, j) {
|
|||||||
|
|
||||||
function wert(feld, i, j) {
|
function wert(feld, i, j) {
|
||||||
if (feld === 'titel') return s.value.titel
|
if (feld === 'titel') return s.value.titel
|
||||||
if (feld === 'ziel') return s.value.ziel
|
if (feld === 'ziel') return i === undefined ? s.value.ziel : s.value.funktionen[i].ziel
|
||||||
if (feld === 'funktion') return s.value.funktionen[i].text
|
if (feld === 'funktion') return s.value.funktionen[i].text
|
||||||
if (feld === 'flow') return s.value.funktionen[i].flow
|
if (feld === 'testen') return s.value.funktionen[i].testen
|
||||||
return s.value.funktionen[i].regeln[j]
|
return i === undefined ? s.value.regeln : s.value.funktionen[i].regeln
|
||||||
}
|
}
|
||||||
|
|
||||||
async function starte(feld, i, j) {
|
async function starte(feld, i, j) {
|
||||||
|
if (!gross.value) return // Bearbeiten nur in der ausführlichen Ansicht
|
||||||
const original = wert(feld, i, j)
|
const original = wert(feld, i, j)
|
||||||
aktiv.value = { feld, i, j, original }
|
aktiv.value = { feld, i, j, original }
|
||||||
entwurf.value = original
|
entwurf.value = original
|
||||||
@@ -59,16 +62,16 @@ async function starte(feld, i, j) {
|
|||||||
function abbrechen() {
|
function abbrechen() {
|
||||||
const a = aktiv.value
|
const a = aktiv.value
|
||||||
aktiv.value = null
|
aktiv.value = null
|
||||||
// frisch angelegte, noch leere Einträge wieder entfernen
|
// frisch angelegte, noch leere Funktion wieder entfernen
|
||||||
if (a && !a.original) {
|
if (a && !a.original && a.feld === 'funktion') {
|
||||||
if (a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
|
s.value.funktionen.splice(a.i, 1)
|
||||||
if (a.feld === 'regel') s.value.funktionen[a.i].regeln.splice(a.j, 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fertig() {
|
async function fertig() {
|
||||||
const a = aktiv.value
|
const a = aktiv.value
|
||||||
if (!a) return
|
if (!a) return
|
||||||
|
blurZeit = Date.now() // der Klick, der das Feld schloss, soll die Ansicht nicht schließen
|
||||||
aktiv.value = null
|
aktiv.value = null
|
||||||
const neu = entwurf.value.trim()
|
const neu = entwurf.value.trim()
|
||||||
|
|
||||||
@@ -78,45 +81,34 @@ async function fertig() {
|
|||||||
if (!neu) return // Titel ist Pflicht — leere Eingabe verwerfen
|
if (!neu) return // Titel ist Pflicht — leere Eingabe verwerfen
|
||||||
s.value.titel = neu
|
s.value.titel = neu
|
||||||
} else if (a.feld === 'ziel') {
|
} else if (a.feld === 'ziel') {
|
||||||
s.value.ziel = neu
|
if (a.i === undefined) s.value.ziel = neu
|
||||||
|
else s.value.funktionen[a.i].ziel = neu
|
||||||
} else if (a.feld === 'funktion') {
|
} else if (a.feld === 'funktion') {
|
||||||
if (!neu) s.value.funktionen.splice(a.i, 1) // leeren = löschen
|
if (!neu) s.value.funktionen.splice(a.i, 1) // leeren = löschen
|
||||||
else s.value.funktionen[a.i].text = neu
|
else s.value.funktionen[a.i].text = neu
|
||||||
} else if (a.feld === 'flow') {
|
} else if (a.feld === 'testen') {
|
||||||
s.value.funktionen[a.i].flow = neu
|
s.value.funktionen[a.i].testen = neu
|
||||||
} else if (a.feld === 'regel') {
|
} else if (a.feld === 'regeln') {
|
||||||
if (!neu) s.value.funktionen[a.i].regeln.splice(a.j, 1)
|
if (a.i === undefined) s.value.regeln = neu
|
||||||
else s.value.funktionen[a.i].regeln[a.j] = neu
|
else s.value.funktionen[a.i].regeln = neu
|
||||||
}
|
}
|
||||||
await speichern()
|
await speichern()
|
||||||
}
|
}
|
||||||
|
|
||||||
function abbrechenLeer(a) {
|
function abbrechenLeer(a) {
|
||||||
if (a.original) return
|
if (!a.original && a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
|
||||||
if (a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
|
|
||||||
if (a.feld === 'regel') s.value.funktionen[a.i].regeln.splice(a.j, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function neueFunktion() {
|
function neueFunktion() {
|
||||||
s.value.funktionen.unshift({ text: '', regeln: [], flow: '' })
|
s.value.funktionen.unshift({ text: '', ziel: '', regeln: '', testen: '' })
|
||||||
starte('funktion', 0)
|
starte('funktion', 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function neueRegel(i) {
|
|
||||||
s.value.funktionen[i].regeln.push('')
|
|
||||||
starte('regel', i, s.value.funktionen[i].regeln.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function entferneFunktion(i) {
|
function entferneFunktion(i) {
|
||||||
s.value.funktionen.splice(i, 1)
|
s.value.funktionen.splice(i, 1)
|
||||||
speichern()
|
speichern()
|
||||||
}
|
}
|
||||||
|
|
||||||
function entferneRegel(i, j) {
|
|
||||||
s.value.funktionen[i].regeln.splice(j, 1)
|
|
||||||
speichern()
|
|
||||||
}
|
|
||||||
|
|
||||||
function wechsleArt() {
|
function wechsleArt() {
|
||||||
s.value.art = s.value.art === 'kern' ? 'optional' : 'kern'
|
s.value.art = s.value.art === 'kern' ? 'optional' : 'kern'
|
||||||
speichern()
|
speichern()
|
||||||
@@ -131,10 +123,13 @@ function baueRoh() {
|
|||||||
`<!-- art -->\n${s.value.art}`,
|
`<!-- art -->\n${s.value.art}`,
|
||||||
`<!-- ziel -->\n${s.value.ziel}`,
|
`<!-- ziel -->\n${s.value.ziel}`,
|
||||||
]
|
]
|
||||||
|
if (s.value.regeln) teile.push(`<!-- regeln -->\n${s.value.regeln}`)
|
||||||
const funktionen = s.value.funktionen
|
const funktionen = s.value.funktionen
|
||||||
.map(f => {
|
.map(f => {
|
||||||
const bloecke = [`<!-- funktion -->\n${f.text}`, ...f.regeln.map(r => `<!-- regel -->\n${r}`)]
|
const bloecke = [`<!-- funktion -->\n${f.text}`]
|
||||||
if (f.flow) bloecke.push(`<!-- flow -->\n${f.flow}`)
|
if (f.ziel) bloecke.push(`<!-- ziel -->\n${f.ziel}`)
|
||||||
|
if (f.regeln) bloecke.push(`<!-- regeln -->\n${f.regeln}`)
|
||||||
|
if (f.testen) bloecke.push(`<!-- testen -->\n${f.testen}`)
|
||||||
return bloecke.join('\n')
|
return bloecke.join('\n')
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
@@ -153,42 +148,33 @@ async function speichern() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loeschBestaetigung = ref(false)
|
|
||||||
|
|
||||||
async function loeschen() {
|
|
||||||
// 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')
|
|
||||||
} catch (e) {
|
|
||||||
meldung.value = e.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function zeilen(text) {
|
function zeilen(text) {
|
||||||
return Math.max(2, text.split('\n').length)
|
return Math.max(2, text.split('\n').length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flow-Text („- Schritt“ je Zeile) für die nummerierte Anzeige zerlegen
|
// Blocktext („- Eintrag“ je Zeile) für die Listen-Anzeige zerlegen
|
||||||
function flowSchritte(flow) {
|
function punkte(text) {
|
||||||
return flow.split('\n').map(z => z.replace(/^[-*]\s*/, '').trim()).filter(Boolean)
|
return text.split('\n').map(z => z.replace(/^[-*]\s*/, '').trim()).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ausführliche Ansicht: Klick auf freie Kartenfläche schaltet um
|
// Standardansicht ist reine Übersicht: Klick öffnet die ausführliche Ansicht,
|
||||||
const gross = ref(false)
|
// nur dort wird bearbeitet; Klick auf freie Fläche schließt sie wieder —
|
||||||
|
// außer der Klick hat gerade eine Bearbeitung beendet (Blur kommt vor dem Klick)
|
||||||
|
let blurZeit = 0
|
||||||
|
|
||||||
function wechsleAnsicht(e) {
|
function wechsleAnsicht(e) {
|
||||||
if (e.target.closest('button, input, textarea, .klickbar, h3')) return
|
if (gross.value) {
|
||||||
gross.value = !gross.value
|
if (e.target.closest('button, input, textarea, .klickbar, h3, .hinweis')) return
|
||||||
|
if (Date.now() - blurZeit < 300) return
|
||||||
|
gross.value = false
|
||||||
|
} else {
|
||||||
|
if (e.target.closest('button, input, textarea')) return
|
||||||
|
gross.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.titelBearbeiten) starte('titel')
|
if (props.vollmodus) gross.value = true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -197,7 +183,7 @@ onMounted(() => {
|
|||||||
class="karte"
|
class="karte"
|
||||||
:class="[s.art, { gross }]"
|
:class="[s.art, { gross }]"
|
||||||
@click="wechsleAnsicht"
|
@click="wechsleAnsicht"
|
||||||
@mouseleave="loeschBestaetigung = false"
|
@contextmenu.prevent.stop="emit('kontextmenue', $event)"
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<input
|
<input
|
||||||
@@ -209,19 +195,15 @@ onMounted(() => {
|
|||||||
@keyup.enter="$event.target.blur()"
|
@keyup.enter="$event.target.blur()"
|
||||||
@keydown.esc="abbrechen"
|
@keydown.esc="abbrechen"
|
||||||
>
|
>
|
||||||
<h3 v-else :title="'Klicken zum Bearbeiten'" @click="starte('titel')">{{ s.titel }}</h3>
|
<h3 v-else :title="gross ? 'Klicken zum Bearbeiten' : null" @click="starte('titel')">{{ s.titel }}</h3>
|
||||||
<button
|
<button
|
||||||
|
v-if="gross"
|
||||||
class="badge"
|
class="badge"
|
||||||
:class="s.art === 'kern' ? 'blau' : 'gruen'"
|
:class="s.art === 'kern' ? 'blau' : 'gruen'"
|
||||||
title="Klicken zum Umschalten"
|
title="Klicken zum Umschalten"
|
||||||
@click="wechsleArt"
|
@click="wechsleArt"
|
||||||
>{{ s.art }}</button>
|
>{{ s.art }}</button>
|
||||||
<button
|
<span v-else class="badge" :class="s.art === 'kern' ? 'blau' : 'gruen'">{{ s.art }}</span>
|
||||||
class="icon-knopf rot werkzeug"
|
|
||||||
:class="{ bestaetigen: loeschBestaetigung }"
|
|
||||||
:title="loeschBestaetigung ? 'Wirklich löschen?' : 'Feature löschen'"
|
|
||||||
@click="loeschen"
|
|
||||||
>{{ loeschBestaetigung ? '✓' : '✕' }}</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
|
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
|
||||||
@@ -235,12 +217,29 @@ onMounted(() => {
|
|||||||
@keydown.esc="abbrechen"
|
@keydown.esc="abbrechen"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="s.ziel" class="inhalt klickbar" v-html="md(s.ziel)" @click="starte('ziel')" />
|
<div v-else-if="s.ziel" class="inhalt klickbar" v-html="md(s.ziel)" @click="starte('ziel')" />
|
||||||
<p v-else class="hinweis" @click="starte('ziel')">Ziel ergänzen…</p>
|
<p v-else-if="gross" class="hinweis" @click="starte('ziel')">Ziel ergänzen…</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Karten-Regeln nur in der ausführlichen Ansicht -->
|
||||||
|
<section v-if="gross" :class="{ leer: !s.regeln && !istAktiv('regeln') }">
|
||||||
|
<p class="gruppe">Regeln</p>
|
||||||
|
<textarea
|
||||||
|
v-if="istAktiv('regeln')"
|
||||||
|
ref="feldRef"
|
||||||
|
v-model="entwurf"
|
||||||
|
:rows="zeilen(entwurf)"
|
||||||
|
@blur="fertig"
|
||||||
|
@keydown.esc="abbrechen"
|
||||||
|
/>
|
||||||
|
<div v-else-if="s.regeln" class="klickbar" @click="starte('regeln')">
|
||||||
|
<div v-for="(r, k) in punkte(s.regeln)" :key="k" class="regel">{{ r }}</div>
|
||||||
|
</div>
|
||||||
|
<p v-else-if="gross" class="hinweis" @click="starte('regeln')">Regeln ergänzen…</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section :class="{ leer: !s.funktionen.length }">
|
<section :class="{ leer: !s.funktionen.length }">
|
||||||
<p class="gruppe">Funktionen</p>
|
<p class="gruppe">Funktionen</p>
|
||||||
<button class="hinzu werkzeug" @click="neueFunktion">+ Funktion</button>
|
<button v-if="gross" class="hinzu werkzeug" @click="neueFunktion">+ Funktion</button>
|
||||||
<div class="liste">
|
<div class="liste">
|
||||||
<div
|
<div
|
||||||
v-for="(f, i) in s.funktionen"
|
v-for="(f, i) in s.funktionen"
|
||||||
@@ -259,44 +258,62 @@ onMounted(() => {
|
|||||||
@keydown.esc="abbrechen"
|
@keydown.esc="abbrechen"
|
||||||
>
|
>
|
||||||
<div v-else class="funktionstitel klickbar" @click="starte('funktion', i)">{{ f.text }}</div>
|
<div v-else class="funktionstitel klickbar" @click="starte('funktion', i)">{{ f.text }}</div>
|
||||||
<button class="icon-knopf rot" title="Funktion entfernen" @click="entferneFunktion(i)">✕</button>
|
<button v-if="gross" class="icon-knopf rot" title="Funktion entfernen" @click="entferneFunktion(i)">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Regeln und Flow nur beim Hover über die Funktion (oder während der Bearbeitung) -->
|
<!-- Regeln und Flow nur beim Hover über die Funktion (oder während der Bearbeitung) -->
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<!-- Regeln als Punktliste -->
|
<!-- Ziel der Funktion -->
|
||||||
<div v-if="f.regeln.length" class="regeln">
|
<div v-if="istAktiv('ziel', i) || f.ziel" class="block">
|
||||||
<div v-for="(r, j) in f.regeln" :key="j" class="zeile">
|
<p class="mini">Ziel</p>
|
||||||
<textarea
|
<textarea
|
||||||
v-if="istAktiv('regel', i, j)"
|
v-if="istAktiv('ziel', i)"
|
||||||
ref="feldRef"
|
ref="feldRef"
|
||||||
v-model="entwurf"
|
v-model="entwurf"
|
||||||
:rows="zeilen(entwurf)"
|
:rows="zeilen(entwurf)"
|
||||||
@blur="fertig"
|
@blur="fertig"
|
||||||
@keydown.esc="abbrechen"
|
@keydown.esc="abbrechen"
|
||||||
/>
|
/>
|
||||||
<div v-else class="regel klickbar" @click="starte('regel', i, j)">{{ r }}</div>
|
<div v-else class="inhalt klickbar" v-html="md(f.ziel)" @click="starte('ziel', i)" />
|
||||||
<button class="icon-knopf rot" title="Regel entfernen" @click="entferneRegel(i, j)">✕</button>
|
</div>
|
||||||
|
<button v-if="!f.ziel && !istAktiv('ziel', i)" class="hinzu" @click="starte('ziel', i)">+ Ziel</button>
|
||||||
|
|
||||||
|
<!-- Regeln als Punktliste, ein Block je Funktion -->
|
||||||
|
<div v-if="istAktiv('regeln', i) || f.regeln" class="block">
|
||||||
|
<p class="mini">Regeln</p>
|
||||||
|
<textarea
|
||||||
|
v-if="istAktiv('regeln', i)"
|
||||||
|
ref="feldRef"
|
||||||
|
v-model="entwurf"
|
||||||
|
:rows="zeilen(entwurf)"
|
||||||
|
@blur="fertig"
|
||||||
|
@keydown.esc="abbrechen"
|
||||||
|
/>
|
||||||
|
<div v-else class="klickbar" @click="starte('regeln', i)">
|
||||||
|
<div v-for="(r, k) in punkte(f.regeln)" :key="k" class="regel">{{ r }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="hinzu" @click="neueRegel(i)">+ Regel</button>
|
<button v-if="!f.regeln && !istAktiv('regeln', i)" class="hinzu" @click="starte('regeln', i)">+ Regeln</button>
|
||||||
|
|
||||||
<!-- Flow als nummerierte Schritte -->
|
<!-- Testen als nummerierte Schritte -->
|
||||||
<textarea
|
<div v-if="istAktiv('testen', i) || f.testen" class="block">
|
||||||
v-if="istAktiv('flow', i)"
|
<p class="mini">Testen</p>
|
||||||
ref="feldRef"
|
<textarea
|
||||||
v-model="entwurf"
|
v-if="istAktiv('testen', i)"
|
||||||
:rows="zeilen(entwurf)"
|
ref="feldRef"
|
||||||
@blur="fertig"
|
v-model="entwurf"
|
||||||
@keydown.esc="abbrechen"
|
:rows="zeilen(entwurf)"
|
||||||
/>
|
@blur="fertig"
|
||||||
<div v-else-if="f.flow" class="flow klickbar" @click="starte('flow', i)">
|
@keydown.esc="abbrechen"
|
||||||
<div v-for="(schritt, k) in flowSchritte(f.flow)" :key="k" class="schritt">
|
/>
|
||||||
<span class="nr">{{ k + 1 }}.</span>{{ schritt }}
|
<div v-else class="klickbar" @click="starte('testen', i)">
|
||||||
|
<div v-for="(schritt, k) in punkte(f.testen)" :key="k" class="schritt">
|
||||||
|
<span class="nr">{{ k + 1 }}.</span>{{ schritt }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button v-if="!f.flow && !istAktiv('flow', i)" class="hinzu" @click="starte('flow', i)">+ Flow</button>
|
<button v-if="!f.testen && !istAktiv('testen', i)" class="hinzu" @click="starte('testen', i)">+ Testen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +331,7 @@ onMounted(() => {
|
|||||||
/* Ausführliche Ansicht: volle Breite, Funktionen in Spalten, alle Details offen.
|
/* Ausführliche Ansicht: volle Breite, Funktionen in Spalten, alle Details offen.
|
||||||
Jede Funktion als eigene Box, Zufüge-Knöpfe nur beim Hover über die Box. */
|
Jede Funktion als eigene Box, Zufüge-Knöpfe nur beim Hover über die Box. */
|
||||||
.karte.gross { column-span: all; display: block; }
|
.karte.gross { column-span: all; display: block; }
|
||||||
.karte.gross .liste { columns: 2; column-gap: 1.2rem; }
|
.karte.gross .liste { columns: 320px; column-gap: 1.2rem; }
|
||||||
.karte.gross section.leer { display: block; }
|
.karte.gross section.leer { display: block; }
|
||||||
.karte.gross .funktion {
|
.karte.gross .funktion {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
@@ -331,9 +348,6 @@ onMounted(() => {
|
|||||||
margin: 0.1rem 0 0;
|
margin: 0.1rem 0 0;
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
}
|
}
|
||||||
.karte.gross .details .hinzu { display: none; }
|
|
||||||
.karte.gross .funktion:hover .details .hinzu { display: block; }
|
|
||||||
.icon-knopf.bestaetigen { color: var(--rot); }
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -343,6 +357,13 @@ header {
|
|||||||
border-bottom: 1px solid var(--rand);
|
border-bottom: 1px solid var(--rand);
|
||||||
}
|
}
|
||||||
h3 { margin: 0; font-size: 1rem; cursor: pointer; flex: 1; }
|
h3 { margin: 0; font-size: 1rem; cursor: pointer; flex: 1; }
|
||||||
|
/* Im Bearbeitungsmodus zeigt Hover die klickbare Titelfläche */
|
||||||
|
.karte.gross h3 {
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
margin-left: -0.3rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.karte.gross h3:hover { background: var(--code-bg); }
|
||||||
.titel-feld { flex: 1; font-weight: 600; padding: 0.1rem 0.4rem; }
|
.titel-feld { flex: 1; font-weight: 600; padding: 0.1rem 0.4rem; }
|
||||||
.badge { cursor: pointer; }
|
.badge { cursor: pointer; }
|
||||||
|
|
||||||
@@ -357,9 +378,8 @@ section + section { border-top: 1px solid var(--rand); }
|
|||||||
margin: 0 0 0.15rem;
|
margin: 0 0 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Leere Sektionen und Werkzeuge erscheinen erst beim Hover über die Karte */
|
/* Leere Sektionen nur in der ausführlichen Ansicht */
|
||||||
section.leer { display: none; }
|
section.leer { display: none; }
|
||||||
.karte:hover section.leer { display: block; }
|
|
||||||
.werkzeug { opacity: 0; transition: opacity 0.1s; }
|
.werkzeug { opacity: 0; transition: opacity 0.1s; }
|
||||||
.karte:hover .werkzeug, .werkzeug:focus { opacity: 1; }
|
.karte:hover .werkzeug, .werkzeug:focus { opacity: 1; }
|
||||||
/* „+ Funktion" belegt ohne Hover auch keinen Platz */
|
/* „+ Funktion" belegt ohne Hover auch keinen Platz */
|
||||||
@@ -369,9 +389,12 @@ section.leer { display: none; }
|
|||||||
.hinweis {
|
.hinweis {
|
||||||
color: var(--dezent);
|
color: var(--dezent);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin: 0.1rem 0;
|
padding: 0.1rem 0.3rem;
|
||||||
|
margin: 0.1rem 0 0.1rem -0.3rem;
|
||||||
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.hinweis:hover { background: var(--code-bg); }
|
||||||
.hinzu {
|
.hinzu {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -391,38 +414,35 @@ section.leer { display: none; }
|
|||||||
.zeile .icon-knopf { font-size: 0.8rem; opacity: 0; }
|
.zeile .icon-knopf { font-size: 0.8rem; opacity: 0; }
|
||||||
.zeile:hover .icon-knopf { opacity: 1; }
|
.zeile:hover .icon-knopf { opacity: 1; }
|
||||||
|
|
||||||
/* Funktionsliste: Zeilen mit Aufklapp-Pfeil */
|
/* Funktionsliste */
|
||||||
.zeile.kopf { padding: 0.15rem 0.2rem; border-radius: 4px; }
|
.zeile.kopf { padding: 0.15rem 0.2rem; border-radius: 4px; }
|
||||||
.zeile.kopf:hover { background: var(--code-bg); }
|
.karte.gross .zeile.kopf:hover { background: var(--code-bg); }
|
||||||
.funktionstitel { flex: 1; }
|
.funktionstitel { flex: 1; }
|
||||||
.pfeil {
|
.pfeil { color: var(--dezent); font-size: 0.75rem; }
|
||||||
color: var(--dezent);
|
|
||||||
font-size: 0.75rem;
|
/* Regeln und Flow nur in der ausführlichen Ansicht */
|
||||||
display: inline-block;
|
.details { display: none; font-size: 0.9rem; }
|
||||||
transition: transform 0.12s;
|
|
||||||
|
/* Regeln (Punktliste) und Flow (nummerierte Schritte) als getönte Blöcke */
|
||||||
|
.block {
|
||||||
|
background: var(--code-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.3rem 0.55rem 0.35rem;
|
||||||
|
margin: 0.3rem 0 0.1rem;
|
||||||
}
|
}
|
||||||
.funktion:hover .pfeil, .funktion.offen .pfeil {
|
.mini {
|
||||||
transform: rotate(90deg);
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
color: var(--akzent);
|
color: var(--akzent);
|
||||||
|
margin: 0 0 0.1rem;
|
||||||
}
|
}
|
||||||
|
.block .klickbar { border-radius: 4px; }
|
||||||
/* Regeln und Flow erst beim Hover über die Funktion; offen bleiben während Bearbeitung */
|
.block .klickbar:hover { background: var(--bg); }
|
||||||
.details { display: none; }
|
.regel, .schritt {
|
||||||
.funktion:hover .details, .funktion.offen .details {
|
|
||||||
display: block;
|
|
||||||
margin: 0.15rem 0 0.45rem 0.35rem;
|
|
||||||
padding: 0.1rem 0 0.1rem 0.75rem;
|
|
||||||
border-left: 2px solid var(--akzent);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Regeln als Punktliste, Flow als nummerierte Schritte */
|
|
||||||
.regeln { margin: 0.15rem 0; }
|
|
||||||
.regel {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 1.15rem;
|
padding-left: 1.15rem;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
.regel::before {
|
.regel::before {
|
||||||
content: "•";
|
content: "•";
|
||||||
@@ -430,25 +450,18 @@ section.leer { display: none; }
|
|||||||
left: 0.35rem;
|
left: 0.35rem;
|
||||||
color: var(--dezent);
|
color: var(--dezent);
|
||||||
}
|
}
|
||||||
.regel:hover { background: var(--code-bg); }
|
|
||||||
/* „+ Regel"/„+ Flow" bündig zur Listen-Einrückung */
|
|
||||||
.details .hinzu {
|
|
||||||
display: block;
|
|
||||||
padding: 0.05rem 0 0.05rem 1.15rem;
|
|
||||||
margin: 0.05rem 0;
|
|
||||||
}
|
|
||||||
.flow { border-radius: 4px; }
|
|
||||||
.flow:hover { background: var(--code-bg); }
|
|
||||||
.schritt {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 1.15rem;
|
|
||||||
}
|
|
||||||
.nr {
|
.nr {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.15rem;
|
left: 0.15rem;
|
||||||
color: var(--dezent);
|
color: var(--dezent);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
/* „+ Regeln"/„+ Flow" bündig zur Listen-Einrückung */
|
||||||
|
.details .hinzu {
|
||||||
|
display: block;
|
||||||
|
padding: 0.05rem 0 0.05rem 0.55rem;
|
||||||
|
margin: 0.05rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { fetchFeatures, featureAnlegen } from '../api.js'
|
import { fetchFeatures, featureAnlegen, featureLoeschen } from '../api.js'
|
||||||
import FeatureKarte from './FeatureKarte.vue'
|
import FeatureKarte from './FeatureKarte.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -10,7 +10,7 @@ const emit = defineEmits(['geaendert'])
|
|||||||
|
|
||||||
const features = ref([])
|
const features = ref([])
|
||||||
const meldung = ref('')
|
const meldung = ref('')
|
||||||
const neuId = ref(null) // frisch angelegte Karte: Titel direkt bearbeiten
|
const neuId = ref(null) // frisch angelegte Karte: direkt im Vollmodus öffnen
|
||||||
|
|
||||||
async function laden() {
|
async function laden() {
|
||||||
meldung.value = ''
|
meldung.value = ''
|
||||||
@@ -23,35 +23,84 @@ async function laden() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function neuesFeature() {
|
function aktualisiert(altId, f) {
|
||||||
|
const idx = features.value.findIndex(x => x.id === altId)
|
||||||
|
if (idx >= 0) features.value[idx] = f
|
||||||
|
if (neuId.value === altId) neuId.value = f.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kontextmenü: Rechtsklick auf eine Card (entfernen) oder den Hauptbereich (erstellen)
|
||||||
|
const kontextMenue = ref(null) // {feature|null, x, y}
|
||||||
|
|
||||||
|
function oeffneKartenMenue(e, f) {
|
||||||
|
kontextMenue.value = { feature: f, x: e.clientX, y: e.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function oeffneBereichsMenue(e) {
|
||||||
|
kontextMenue.value = { feature: null, x: e.clientX, y: e.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function schliesseKontextMenue() {
|
||||||
|
kontextMenue.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
|
||||||
|
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
|
||||||
|
|
||||||
|
// Modal für Erstellen und Entfernen
|
||||||
|
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', feature}
|
||||||
|
const modalName = ref('')
|
||||||
|
const modalFeld = ref(null)
|
||||||
|
|
||||||
|
async function menueErstellen() {
|
||||||
|
kontextMenue.value = null
|
||||||
meldung.value = ''
|
meldung.value = ''
|
||||||
|
modal.value = { art: 'erstellen' }
|
||||||
|
modalName.value = ''
|
||||||
|
await nextTick()
|
||||||
|
modalFeld.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function menueEntfernen() {
|
||||||
|
const feature = kontextMenue.value.feature
|
||||||
|
kontextMenue.value = null
|
||||||
|
meldung.value = ''
|
||||||
|
modal.value = { art: 'entfernen', feature }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function erstellenBestaetigen() {
|
||||||
|
meldung.value = ''
|
||||||
|
const name = modalName.value.trim()
|
||||||
|
if (!name) return
|
||||||
try {
|
try {
|
||||||
const f = await featureAnlegen(props.projekt, '<!-- titel -->\nNeues Feature\n')
|
const f = await featureAnlegen(props.projekt, `<!-- titel -->\n${name}\n`)
|
||||||
|
modal.value = null
|
||||||
features.value.push(f)
|
features.value.push(f)
|
||||||
neuId.value = f.id
|
neuId.value = f.id // öffnet die Karte im Vollmodus
|
||||||
emit('geaendert') // Anzahl in der Seitenleiste aktualisieren
|
emit('geaendert')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
meldung.value = e.message
|
meldung.value = e.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function aktualisiert(altId, f) {
|
async function entfernenBestaetigen() {
|
||||||
const idx = features.value.findIndex(x => x.id === altId)
|
meldung.value = ''
|
||||||
if (idx >= 0) features.value[idx] = f
|
try {
|
||||||
if (neuId.value === altId) neuId.value = null
|
await featureLoeschen(props.projekt, modal.value.feature.id)
|
||||||
}
|
features.value = features.value.filter(f => f.id !== modal.value.feature.id)
|
||||||
|
modal.value = null
|
||||||
function geloescht(id) {
|
emit('geaendert')
|
||||||
features.value = features.value.filter(f => f.id !== id)
|
} catch (e) {
|
||||||
emit('geaendert')
|
meldung.value = e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.projekt, laden, { immediate: true })
|
watch(() => props.projekt, laden, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="bereich" @contextmenu.prevent="oeffneBereichsMenue">
|
||||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
<p v-if="meldung && !modal" class="meldung">{{ meldung }}</p>
|
||||||
|
|
||||||
<div class="gitter">
|
<div class="gitter">
|
||||||
<FeatureKarte
|
<FeatureKarte
|
||||||
@@ -59,16 +108,54 @@ watch(() => props.projekt, laden, { immediate: true })
|
|||||||
:key="f.id"
|
:key="f.id"
|
||||||
:projekt="projekt"
|
:projekt="projekt"
|
||||||
:feature="f"
|
:feature="f"
|
||||||
:titel-bearbeiten="f.id === neuId"
|
:vollmodus="f.id === neuId"
|
||||||
@aktualisiert="aktualisiert"
|
@aktualisiert="aktualisiert"
|
||||||
@geloescht="geloescht(f.id)"
|
@kontextmenue="oeffneKartenMenue($event, f)"
|
||||||
/>
|
/>
|
||||||
<button class="karte neu" @click="neuesFeature">+ Feature</button>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="kontextMenue"
|
||||||
|
class="kontextmenue"
|
||||||
|
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||||
|
>
|
||||||
|
<button v-if="kontextMenue.feature" @click="menueEntfernen">✕ Feature entfernen</button>
|
||||||
|
<button v-else @click="menueErstellen">+ Feature erstellen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modal" class="schleier" @click.self="modal = null">
|
||||||
|
<div class="modal karte">
|
||||||
|
<template v-if="modal.art === 'erstellen'">
|
||||||
|
<h2>Feature erstellen</h2>
|
||||||
|
<input
|
||||||
|
ref="modalFeld"
|
||||||
|
v-model="modalName"
|
||||||
|
placeholder="Feature-Titel"
|
||||||
|
@keyup.enter="erstellenBestaetigen"
|
||||||
|
@keydown.esc="modal = null"
|
||||||
|
>
|
||||||
|
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||||
|
<footer>
|
||||||
|
<button @click="modal = null">Abbrechen</button>
|
||||||
|
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h2>Feature entfernen</h2>
|
||||||
|
<p class="frage">„{{ modal.feature.sektionen.titel }}“ entfernen?</p>
|
||||||
|
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||||
|
<footer>
|
||||||
|
<button @click="modal = null">Abbrechen</button>
|
||||||
|
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.bereich { min-height: 100%; }
|
||||||
/* Spaltenlayout: unterschiedlich hohe Karten stapeln sich ohne Lücken */
|
/* Spaltenlayout: unterschiedlich hohe Karten stapeln sich ohne Lücken */
|
||||||
.gitter {
|
.gitter {
|
||||||
columns: 340px;
|
columns: 340px;
|
||||||
@@ -83,13 +170,4 @@ watch(() => props.projekt, laden, { immediate: true })
|
|||||||
margin-bottom: 0.9rem;
|
margin-bottom: 0.9rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
/* Geisterkarte statt Kopfzeilen-Knopf */
|
|
||||||
.neu {
|
|
||||||
border: 1px dashed var(--rand);
|
|
||||||
background: none;
|
|
||||||
color: var(--dezent);
|
|
||||||
min-height: 90px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.neu:hover { color: var(--akzent); border-color: var(--akzent); }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, ref } from 'vue'
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { projektAnlegen, projektUmbenennen, projektLoeschen } from '../api.js'
|
import { projektAnlegen, projektUmbenennen, projektLoeschen } from '../api.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -8,26 +8,10 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
const emit = defineEmits(['auswaehlen', 'geaendert', 'umbenannt'])
|
const emit = defineEmits(['auswaehlen', 'geaendert', 'umbenannt'])
|
||||||
|
|
||||||
const neuerName = ref('')
|
|
||||||
const meldung = ref('')
|
const meldung = ref('')
|
||||||
const bearbeitet = ref(null) // Projektname, der gerade umbenannt wird
|
const bearbeitet = ref(null) // Projektname, der gerade umbenannt wird
|
||||||
const umbenennung = ref('')
|
const umbenennung = ref('')
|
||||||
const umbenennFeld = ref(null)
|
const umbenennFeld = ref(null)
|
||||||
const loeschBestaetigung = ref(null) // Projektname, dessen ✕ auf Bestätigung wartet
|
|
||||||
|
|
||||||
async function anlegen() {
|
|
||||||
meldung.value = ''
|
|
||||||
const name = neuerName.value.trim()
|
|
||||||
if (!name) return
|
|
||||||
try {
|
|
||||||
await projektAnlegen(name)
|
|
||||||
neuerName.value = ''
|
|
||||||
emit('geaendert')
|
|
||||||
emit('auswaehlen', name)
|
|
||||||
} catch (e) {
|
|
||||||
meldung.value = e.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function starteUmbenennen(name) {
|
async function starteUmbenennen(name) {
|
||||||
meldung.value = ''
|
meldung.value = ''
|
||||||
@@ -49,16 +33,71 @@ async function umbenennen(alt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loeschen(name) {
|
// Kontextmenü: Rechtsklick auf Projektname (anpassen/entfernen)
|
||||||
// Erster Klick: Icon wird zum Bestätigungs-Icon, zweiter Klick löscht
|
// oder auf freie Sidebar-Fläche (erstellen)
|
||||||
if (loeschBestaetigung.value !== name) {
|
const kontextMenue = ref(null) // {name|null, x, y}
|
||||||
loeschBestaetigung.value = name
|
|
||||||
return
|
function oeffneProjektMenue(e, name) {
|
||||||
|
kontextMenue.value = { name, x: e.clientX, y: e.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function oeffneLeistenMenue(e) {
|
||||||
|
kontextMenue.value = { name: null, x: e.clientX, y: e.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function schliesseKontextMenue() {
|
||||||
|
kontextMenue.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
|
||||||
|
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
|
||||||
|
|
||||||
|
// Modal für Erstellen und Entfernen
|
||||||
|
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', name}
|
||||||
|
const modalName = ref('')
|
||||||
|
const modalFeld = ref(null)
|
||||||
|
|
||||||
|
async function menueErstellen() {
|
||||||
|
kontextMenue.value = null
|
||||||
|
meldung.value = ''
|
||||||
|
modal.value = { art: 'erstellen' }
|
||||||
|
modalName.value = ''
|
||||||
|
await nextTick()
|
||||||
|
modalFeld.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function menueAnpassen() {
|
||||||
|
const name = kontextMenue.value.name
|
||||||
|
kontextMenue.value = null
|
||||||
|
starteUmbenennen(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function menueEntfernen() {
|
||||||
|
const name = kontextMenue.value.name
|
||||||
|
kontextMenue.value = null
|
||||||
|
meldung.value = ''
|
||||||
|
modal.value = { art: 'entfernen', name }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function erstellenBestaetigen() {
|
||||||
|
meldung.value = ''
|
||||||
|
const name = modalName.value.trim()
|
||||||
|
if (!name) return
|
||||||
|
try {
|
||||||
|
await projektAnlegen(name)
|
||||||
|
modal.value = null
|
||||||
|
emit('geaendert')
|
||||||
|
emit('auswaehlen', name) // Projekt wird direkt aufgerufen
|
||||||
|
} catch (e) {
|
||||||
|
meldung.value = e.message
|
||||||
}
|
}
|
||||||
loeschBestaetigung.value = null
|
}
|
||||||
|
|
||||||
|
async function entfernenBestaetigen() {
|
||||||
meldung.value = ''
|
meldung.value = ''
|
||||||
try {
|
try {
|
||||||
await projektLoeschen(name)
|
await projektLoeschen(modal.value.name)
|
||||||
|
modal.value = null
|
||||||
emit('geaendert')
|
emit('geaendert')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
meldung.value = e.message
|
meldung.value = e.message
|
||||||
@@ -67,7 +106,7 @@ async function loeschen(name) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside>
|
<aside @contextmenu.prevent="oeffneLeistenMenue">
|
||||||
<header>
|
<header>
|
||||||
<p class="gruppe">Projekte</p>
|
<p class="gruppe">Projekte</p>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -78,7 +117,7 @@ async function loeschen(name) {
|
|||||||
:key="p.name"
|
:key="p.name"
|
||||||
:class="{ aktiv: p.name === aktiv }"
|
:class="{ aktiv: p.name === aktiv }"
|
||||||
@click="emit('auswaehlen', p.name)"
|
@click="emit('auswaehlen', p.name)"
|
||||||
@mouseleave="loeschBestaetigung = null"
|
@contextmenu.prevent.stop="oeffneProjektMenue($event, p.name)"
|
||||||
>
|
>
|
||||||
<template v-if="bearbeitet === p.name">
|
<template v-if="bearbeitet === p.name">
|
||||||
<input
|
<input
|
||||||
@@ -92,25 +131,51 @@ async function loeschen(name) {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="name">{{ p.name }}</span>
|
<span class="name">{{ p.name }}</span>
|
||||||
<span class="anzahl">{{ p.anzahl }}</span>
|
|
||||||
<span class="aktionen" @click.stop>
|
|
||||||
<button class="icon-knopf" title="Umbenennen" @click="starteUmbenennen(p.name)">✎</button>
|
|
||||||
<button
|
|
||||||
class="icon-knopf rot"
|
|
||||||
:class="{ bestaetigen: loeschBestaetigung === p.name }"
|
|
||||||
:title="loeschBestaetigung === p.name ? 'Wirklich löschen?' : 'Löschen'"
|
|
||||||
@click="loeschen(p.name)"
|
|
||||||
>{{ loeschBestaetigung === p.name ? '✓' : '✕' }}</button>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p v-if="meldung && !modal" class="meldung">{{ meldung }}</p>
|
||||||
|
|
||||||
<form class="neu" @submit.prevent="anlegen">
|
<div
|
||||||
<input v-model="neuerName" placeholder="Neues Projekt">
|
v-if="kontextMenue"
|
||||||
<button class="primaer" :disabled="!neuerName.trim()">+</button>
|
class="kontextmenue"
|
||||||
</form>
|
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
>
|
||||||
|
<template v-if="kontextMenue.name">
|
||||||
|
<button @click="menueAnpassen">✎ Projekt anpassen</button>
|
||||||
|
<button @click="menueEntfernen">✕ Projekt entfernen</button>
|
||||||
|
</template>
|
||||||
|
<button v-else @click="menueErstellen">+ Projekt erstellen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modal" class="schleier" @click.self="modal = null">
|
||||||
|
<div class="modal karte">
|
||||||
|
<template v-if="modal.art === 'erstellen'">
|
||||||
|
<h2>Projekt erstellen</h2>
|
||||||
|
<input
|
||||||
|
ref="modalFeld"
|
||||||
|
v-model="modalName"
|
||||||
|
placeholder="Projektname"
|
||||||
|
@keyup.enter="erstellenBestaetigen"
|
||||||
|
@keydown.esc="modal = null"
|
||||||
|
>
|
||||||
|
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||||
|
<footer>
|
||||||
|
<button @click="modal = null">Abbrechen</button>
|
||||||
|
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h2>Projekt entfernen</h2>
|
||||||
|
<p class="frage">„{{ modal.name }}“ mit allen Features entfernen?</p>
|
||||||
|
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||||
|
<footer>
|
||||||
|
<button @click="modal = null">Abbrechen</button>
|
||||||
|
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -154,16 +219,6 @@ li.aktiv { background: var(--bg); font-weight: 600; }
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.anzahl { color: var(--dezent); font-size: 0.78rem; }
|
|
||||||
.aktionen { display: none; }
|
|
||||||
li:hover .aktionen { display: inline-flex; }
|
|
||||||
li:hover .anzahl { display: none; }
|
|
||||||
li input { width: 100%; padding: 0.1rem 0.4rem; }
|
li input { width: 100%; padding: 0.1rem 0.4rem; }
|
||||||
.icon-knopf.bestaetigen { color: var(--rot); }
|
|
||||||
.neu {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
}
|
|
||||||
.neu input { flex: 1; min-width: 0; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user