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

@@ -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; }