This commit is contained in:
Team3
2026-07-26 13:06:45 +02:00
parent 92889711a0
commit 714307f93e
11 changed files with 459 additions and 117 deletions

View File

@@ -12,8 +12,8 @@ Rein manuelle Pflege: Sidebar links = Projekte, Hauptbereich = Feature-Cards.
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.
Format (Marker allein auf einer Zeile; `<!-- funktion -->` ist wiederholbar, jede
`<!-- regel -->` gehört zur davorstehenden Funktion; Trennzeilen aus `---` sind Optik):
Format (Marker allein auf einer Zeile; `<!-- funktion -->` ist wiederholbar, `<!-- regel -->`
und `<!-- flow -->` gehören zur davorstehenden Funktion; Trennzeilen aus `---` sind Optik):
```markdown
<!-- titel -->
@@ -26,14 +26,16 @@ Kurzer Satz, was das Ziel ist.
Anmeldung mit E-Mail
<!-- regel -->
Passwort nie loggen
<!-- flow -->
- Formular öffnen
- Daten eingeben
<!-- funktion -->
Abmeldung
<!-- sonstiges -->
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.

View File

@@ -1,8 +1,9 @@
"""Zerlegt den rohen Feature-Text in Sektionen — nur für die Anzeige.
Format: titel (eine Zeile), ziel, dann wiederholbare <!-- funktion -->-Blöcke,
denen <!-- regel -->-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 <!-- funktion -->-Blöcke;
<!-- regel -->- und <!-- flow -->-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"<!-- funktion -->\n{f['text']}")
for r in f["regeln"]:
teile.append(f"<!-- regel -->\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),
}

View File

@@ -3,6 +3,9 @@ from parser import zerlege
VOLL = """<!-- titel -->
Login
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Nutzer können sich anmelden.
----------------------------------------
@@ -12,33 +15,32 @@ Anmeldung mit E-Mail
Passwort nie loggen
<!-- regel -->
Sperre nach 5 Fehlversuchen
<!-- flow -->
- Formular öffnen
- Daten eingeben
<!-- funktion -->
Abmeldung
----------------------------------------
<!-- sonstiges -->
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"] == (
"<!-- funktion -->\nAnmeldung mit E-Mail\n"
"<!-- regel -->\nPasswort nie loggen\n"
"<!-- regel -->\nSperre nach 5 Fehlversuchen\n"
"<!-- funktion -->\nAbmeldung"
)
def test_mehrere_flow_bloecke_werden_verbunden():
s = zerlege("<!-- funktion -->\nX\n<!-- flow -->\n- a\n<!-- flow -->\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("<!-- regel -->\nverwaist\n")
def test_regel_und_flow_ohne_funktion_landen_in_sonstiges():
s = zerlege("<!-- regel -->\nverwaist\n<!-- flow -->\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():

19
daten/Planer/darkmode.md Normal file
View File

@@ -0,0 +1,19 @@
<!-- titel -->
Darkmode
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
----------------------------------------
<!-- funktion -->
Darkmode umschalten
<!-- regel -->
Icon: ☾ / ☀
<!-- regel -->
Folgt beim ersten Start der System-Einstellung
<!-- regel -->
Zustand bleibt gespeichert
<!-- flow -->
- Klick auf Icon in der Sidebar oben

78
daten/Planer/features.md Normal file
View File

@@ -0,0 +1,78 @@
<!-- titel -->
Features
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
----------------------------------------
<!-- funktion -->
Ausführliche Ansicht umschalten
<!-- regel -->
Volle Breite
<!-- regel -->
Funktionen als Boxen in zwei Spalten
<!-- regel -->
Alle Details offen
<!-- flow -->
- Klick auf freie Kartenfläche
- Nochmal klicken schaltet zurück
<!-- funktion -->
Feature als Datei speichern
<!-- regel -->
Projekt = Ordner unter daten/
<!-- regel -->
Feature = Markdown-Datei im Projektordner
<!-- regel -->
Titeländerung benennt die Datei um
<!-- regel -->
Versionierbar per git
<!-- funktion -->
Feature erstellen
<!-- regel -->
Icon: +
<!-- regel -->
Titel direkt zur Bearbeitung geöffnet
<!-- regel -->
Karten alphabetisch nach Titel sortiert
<!-- flow -->
- Klick auf leere Card
- Card wird erstellt
<!-- funktion -->
Features entfernen
<!-- regel -->
Icon: x
<!-- flow -->
- Hover über Card
- Auf Icon klicken
- Icon wird zum Bestätigungs Icon
- Klick zum entfernen
<!-- funktion -->
Kern/Optional umschalten
<!-- regel -->
Kern blau umrandet
<!-- regel -->
Optional grün umrandet
<!-- flow -->
- Klick auf Badge
<!-- funktion -->
Titel anpassen
<!-- regel -->
Enter schließt ab
<!-- regel -->
Verlassen speichert
<!-- regel -->
Escape verwirft
<!-- flow -->
- Hover über Card
- Klick auf Titel
- Inhalt ändern
<!-- funktion -->
Ziel anpassen
<!-- regel -->
Leer: Platzhalter „Ziel ergänzen" klicken
<!-- flow -->
- Hover über Card
- Klick auf Zieltext
- Inhalt anpassen

View File

@@ -0,0 +1,59 @@
<!-- titel -->
Funktionen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
----------------------------------------
<!-- funktion -->
Flow anpassen
<!-- regel -->
Ein Flow je Funktion
<!-- regel -->
Leeren entfernt den Flow
<!-- flow -->
- Hover über Funktion
- Klick auf Flow oder + Flow
- Inhalt anpassen
<!-- funktion -->
Funktion anpassen
<!-- flow -->
- Klick auf Text
- Inhalt ändern
<!-- funktion -->
Funktion entfernen
<!-- regel -->
Feld leeren entfernt auch
<!-- flow -->
- Hover über Funktion
- Klick auf x
<!-- funktion -->
Funktion hinzufügen
<!-- regel -->
Alphabetisch sortiert
<!-- regel -->
Neue Funktion rückt nach dem Speichern an ihre Position
<!-- flow -->
- Hover über Card
- Klick auf + Funktion
- Inhalt eingeben
<!-- funktion -->
Regel anpassen
<!-- flow -->
- Klick auf Text
- Inhalt ändern
<!-- funktion -->
Regel entfernen
<!-- regel -->
Feld leeren entfernt auch
<!-- flow -->
- Hover über Regel
- Klick auf x
<!-- funktion -->
Regel hinzufügen
<!-- flow -->
- Hover über Funktion
- Klick auf + Regel
- Inhalt eingeben

47
daten/Planer/projekte.md Normal file
View File

@@ -0,0 +1,47 @@
<!-- titel -->
Projekte
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
----------------------------------------
<!-- funktion -->
Projekt anpassen
<!-- regel -->
Icon: Stift
<!-- regel -->
Enter bestätigt
<!-- regel -->
Escape bricht ab
<!-- flow -->
- Hover über Projektname
- Klick auf Icon
- Inhalt anpassen
<!-- funktion -->
Projekt auswählen
<!-- regel -->
Aktives Projekt ist hervorgehoben
<!-- flow -->
- Klick auf Name
- Features werden im Hauptbereich angezeigt
<!-- funktion -->
Projekt entfernen
<!-- regel -->
Icon: x
<!-- flow -->
- Hover über Name
- Klick auf Icon
- Icon wird zum Bestätigungs Icon
- Klick zum entfernen
<!-- funktion -->
Projekt erstellen
<!-- regel -->
Alphabetisch in der Sidebar sortiert
<!-- regel -->
Feature-Anzahl neben dem Namen
<!-- flow -->
- Inputfeld titel eingeben
- Mit Icon oder Enter erstellen
- Projekt wird direkt ausgewählt

View File

@@ -1,11 +0,0 @@
<!-- titel -->
Neues Feature
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
adsda
----------------------------------------
<!-- sonstiges -->

View File

@@ -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() {
`<!-- ziel -->\n${s.value.ziel}`,
]
const funktionen = s.value.funktionen
.map(f => [`<!-- funktion -->\n${f.text}`, ...f.regeln.map(r => `<!-- regel -->\n${r}`)].join('\n'))
.map(f => {
const bloecke = [`<!-- funktion -->\n${f.text}`, ...f.regeln.map(r => `<!-- regel -->\n${r}`)]
if (f.flow) bloecke.push(`<!-- flow -->\n${f.flow}`)
return bloecke.join('\n')
})
.join('\n')
if (funktionen) teile.push(funktionen)
teile.push(`<!-- sonstiges -->\n${s.value.sonstiges}`)
if (s.value.sonstiges) teile.push(`<!-- sonstiges -->\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')
})
</script>
<template>
<article class="karte" :class="s.art">
<article
class="karte"
:class="[s.art, { gross }]"
@click="wechsleAnsicht"
@mouseleave="loeschBestaetigung = false"
>
<header>
<input
v-if="istAktiv('titel')"
@@ -185,7 +216,12 @@ onMounted(() => {
title="Klicken zum Umschalten"
@click="wechsleArt"
>{{ s.art }}</button>
<button class="icon-knopf rot werkzeug" title="Feature löschen" @click="loeschen"></button>
<button
class="icon-knopf rot werkzeug"
:class="{ bestaetigen: loeschBestaetigung }"
:title="loeschBestaetigung ? 'Wirklich löschen?' : 'Feature löschen'"
@click="loeschen"
>{{ loeschBestaetigung ? '✓' : '✕' }}</button>
</header>
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
@@ -204,50 +240,66 @@ onMounted(() => {
<section :class="{ leer: !s.funktionen.length }">
<p class="gruppe">Funktionen</p>
<div v-for="(f, i) in s.funktionen" :key="i" class="funktion">
<div class="zeile">
<textarea
<button class="hinzu werkzeug" @click="neueFunktion">+ Funktion</button>
<div class="liste">
<div
v-for="(f, i) in s.funktionen"
:key="i"
class="funktion"
:class="{ offen: aktiv && aktiv.i === i }"
>
<div class="zeile kopf">
<span class="pfeil"></span>
<input
v-if="istAktiv('funktion', i)"
ref="feldRef"
v-model="entwurf"
@blur="fertig"
@keyup.enter="$event.target.blur()"
@keydown.esc="abbrechen"
>
<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>
</div>
<!-- Regeln und Flow nur beim Hover über die Funktion (oder während der Bearbeitung) -->
<div class="details">
<!-- Regeln als Punktliste -->
<div v-if="f.regeln.length" class="regeln">
<div v-for="(r, j) in f.regeln" :key="j" class="zeile">
<textarea
v-if="istAktiv('regel', i, j)"
ref="feldRef"
v-model="entwurf"
:rows="zeilen(entwurf)"
@blur="fertig"
@keydown.esc="abbrechen"
/>
<div v-else class="regel klickbar" @click="starte('regel', i, j)">{{ r }}</div>
<button class="icon-knopf rot" title="Regel entfernen" @click="entferneRegel(i, j)"></button>
</div>
</div>
<button class="hinzu" @click="neueRegel(i)">+ Regel</button>
<!-- Flow als nummerierte Schritte -->
<textarea
v-if="istAktiv('flow', i)"
ref="feldRef"
v-model="entwurf"
:rows="zeilen(entwurf)"
@blur="fertig"
@keydown.esc="abbrechen"
/>
<div v-else class="inhalt klickbar" v-html="md(f.text)" @click="starte('funktion', i)" />
<button class="icon-knopf rot werkzeug" title="Funktion entfernen" @click="entferneFunktion(i)"></button>
</div>
<div class="regeln">
<div v-for="(r, j) in f.regeln" :key="j" class="zeile">
<textarea
v-if="istAktiv('regel', i, j)"
ref="feldRef"
v-model="entwurf"
:rows="zeilen(entwurf)"
@blur="fertig"
@keydown.esc="abbrechen"
/>
<div v-else class="inhalt klickbar" v-html="md(r)" @click="starte('regel', i, j)" />
<button class="icon-knopf rot werkzeug" title="Regel entfernen" @click="entferneRegel(i, j)"></button>
<div v-else-if="f.flow" class="flow klickbar" @click="starte('flow', i)">
<div v-for="(schritt, k) in flowSchritte(f.flow)" :key="k" class="schritt">
<span class="nr">{{ k + 1 }}.</span>{{ schritt }}
</div>
</div>
<button class="hinzu werkzeug" @click="neueRegel(i)">+ Regel</button>
<button v-if="!f.flow && !istAktiv('flow', i)" class="hinzu" @click="starte('flow', i)">+ Flow</button>
</div>
</div>
<button class="hinzu werkzeug" @click="neueFunktion">+ Funktion</button>
</section>
<section :class="{ leer: !s.sonstiges && !istAktiv('sonstiges') }">
<p class="gruppe">Sonstiges</p>
<textarea
v-if="istAktiv('sonstiges')"
ref="feldRef"
v-model="entwurf"
:rows="zeilen(entwurf)"
@blur="fertig"
@keydown.esc="abbrechen"
/>
<div v-else-if="s.sonstiges" class="inhalt klickbar" v-html="md(s.sonstiges)" @click="starte('sonstiges')" />
<p v-else class="hinweis" @click="starte('sonstiges')">Sonstiges ergänzen</p>
</div>
</section>
<p v-if="meldung" class="meldung">{{ meldung }}</p>
@@ -255,9 +307,34 @@ onMounted(() => {
</template>
<style scoped>
.karte { cursor: pointer; }
.karte.kern { border-color: var(--akzent); }
.karte.optional { border-color: var(--gruen); }
/* 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. */
.karte.gross { column-span: all; display: block; }
.karte.gross .liste { columns: 2; column-gap: 1.2rem; }
.karte.gross section.leer { display: block; }
.karte.gross .funktion {
break-inside: avoid;
border: 1px solid var(--rand);
border-radius: 8px;
padding: 0.35rem 0.6rem 0.45rem;
margin-bottom: 0.6rem;
}
.karte.gross .pfeil { display: none; }
.karte.gross .zeile.kopf { font-weight: 600; padding-left: 0; }
.karte.gross .details {
display: block;
border-left: none;
margin: 0.1rem 0 0;
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 {
display: flex;
align-items: baseline;
@@ -265,9 +342,9 @@ header {
padding-bottom: 0.55rem;
border-bottom: 1px solid var(--rand);
}
h3 { margin: 0; font-size: 1rem; cursor: text; flex: 1; }
.badge { cursor: pointer; }
h3 { margin: 0; font-size: 1rem; cursor: pointer; flex: 1; }
.titel-feld { flex: 1; font-weight: 600; padding: 0.1rem 0.4rem; }
.badge { cursor: pointer; }
section { padding: 0.6rem 0; }
section + section { border-top: 1px solid var(--rand); }
@@ -285,7 +362,7 @@ section.leer { display: none; }
.karte:hover section.leer { display: block; }
.werkzeug { opacity: 0; transition: opacity 0.1s; }
.karte:hover .werkzeug, .werkzeug:focus { opacity: 1; }
/* „+ Funktion"/„+ Regel" belegen ohne Hover auch keinen Platz */
/* „+ Funktion" belegt ohne Hover auch keinen Platz */
.hinzu.werkzeug { display: none; }
.karte:hover .hinzu.werkzeug { display: block; }
@@ -293,7 +370,7 @@ section.leer { display: none; }
color: var(--dezent);
font-size: 0.85rem;
margin: 0.1rem 0;
cursor: text;
cursor: pointer;
}
.hinzu {
border: none;
@@ -309,22 +386,70 @@ section.leer { display: none; }
align-items: baseline;
gap: 0.3rem;
}
.zeile .inhalt, .zeile textarea { flex: 1; min-width: 0; }
.zeile .icon-knopf { font-size: 0.8rem; }
.zeile .inhalt, .zeile input, .zeile textarea { flex: 1; min-width: 0; }
/* ✕ nur auf der Zeile, über der die Maus steht */
.zeile .icon-knopf { font-size: 0.8rem; opacity: 0; }
.zeile:hover .icon-knopf { opacity: 1; }
.funktion + .funktion {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px dashed var(--rand);
}
.regeln {
margin: 0.25rem 0 0 0.3rem;
padding-left: 0.6rem;
border-left: 2px solid var(--rand);
/* Funktionsliste: Zeilen mit Aufklapp-Pfeil */
.zeile.kopf { padding: 0.15rem 0.2rem; border-radius: 4px; }
.zeile.kopf:hover { background: var(--code-bg); }
.funktionstitel { flex: 1; }
.pfeil {
color: var(--dezent);
font-size: 0.75rem;
display: inline-block;
transition: transform 0.12s;
}
.funktion:hover .pfeil, .funktion.offen .pfeil {
transform: rotate(90deg);
color: var(--akzent);
}
/* Regeln und Flow erst beim Hover über die Funktion; offen bleiben während Bearbeitung */
.details { display: none; }
.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;
padding-left: 1.15rem;
border-radius: 4px;
}
.regel::before {
content: "•";
position: absolute;
left: 0.35rem;
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 {
position: absolute;
left: 0.15rem;
color: var(--dezent);
font-size: 0.85em;
}
textarea {
width: 100%;
resize: vertical;
@@ -332,8 +457,10 @@ textarea {
font-size: 0.85rem;
line-height: 1.5;
}
.klickbar { cursor: text; border-radius: 4px; }
.klickbar:hover { background: var(--code-bg); }
.klickbar { cursor: pointer; user-select: none; }
.inhalt.klickbar { border-radius: 4px; }
.inhalt.klickbar:hover { background: var(--code-bg); }
h3, .hinweis { user-select: none; }
.inhalt :deep(p) { margin: 0.2rem 0; }
.inhalt :deep(ul) { margin: 0.2rem 0; padding-left: 1.2rem; }

View File

@@ -69,11 +69,19 @@ watch(() => props.projekt, laden, { immediate: true })
</template>
<style scoped>
/* Spaltenlayout: unterschiedlich hohe Karten stapeln sich ohne Lücken */
.gitter {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 0.9rem;
align-items: start;
columns: 340px;
column-gap: 0.9rem;
}
.gitter > * {
/* inline-block macht die Karte atomar — Firefox fragmentiert sie sonst
trotz break-inside: avoid mitten im Inhalt */
break-inside: avoid;
display: inline-block;
vertical-align: top;
margin-bottom: 0.9rem;
width: 100%;
}
/* Geisterkarte statt Kopfzeilen-Knopf */
.neu {

View File

@@ -13,6 +13,7 @@ const meldung = ref('')
const bearbeitet = ref(null) // Projektname, der gerade umbenannt wird
const umbenennung = ref('')
const umbenennFeld = ref(null)
const loeschBestaetigung = ref(null) // Projektname, dessen ✕ auf Bestätigung wartet
async function anlegen() {
meldung.value = ''
@@ -49,8 +50,13 @@ async function umbenennen(alt) {
}
async function loeschen(name) {
// Erster Klick: Icon wird zum Bestätigungs-Icon, zweiter Klick löscht
if (loeschBestaetigung.value !== name) {
loeschBestaetigung.value = name
return
}
loeschBestaetigung.value = null
meldung.value = ''
if (!confirm(`Projekt „${name}“ mit allen Features löschen?`)) return
try {
await projektLoeschen(name)
emit('geaendert')
@@ -72,6 +78,7 @@ async function loeschen(name) {
:key="p.name"
:class="{ aktiv: p.name === aktiv }"
@click="emit('auswaehlen', p.name)"
@mouseleave="loeschBestaetigung = null"
>
<template v-if="bearbeitet === p.name">
<input
@@ -88,7 +95,12 @@ async function loeschen(name) {
<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" title="Löschen" @click="loeschen(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>
</li>
@@ -147,6 +159,7 @@ li.aktiv { background: var(--bg); font-weight: 600; }
li:hover .aktionen { display: inline-flex; }
li:hover .anzahl { display: none; }
li input { width: 100%; padding: 0.1rem 0.4rem; }
.icon-knopf.bestaetigen { color: var(--rot); }
.neu {
display: flex;
gap: 0.4rem;