This commit is contained in:
Team3
2026-07-26 20:15:08 +02:00
parent 6b155affbd
commit ebfb2fc360
6 changed files with 190 additions and 88 deletions

View File

@@ -5,13 +5,18 @@ Darkmode
optional optional
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Zwischen Tag- und Nachtmodus wechseln
----------------------------------------
<!-- regeln -->
- Standard sind die Systemeinstellungen
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Darkmode umschalten Darkmode umschalten
<!-- regeln --> <!-- regeln -->
- Icon: ☾ / ☀ - Nacht-Icon: ☾
- Folgt beim ersten Start der System-Einstellung - Tag-Icon: ☀
- Auto-Icon: A
- Zustand bleibt gespeichert - Zustand bleibt gespeichert
<!-- testen --> <!-- testen -->
- Klick auf Icon in der Sidebar oben - LMB auf Icon in der Sidebar oben
- Modus schaltet weiter: Auto, Tag, Nacht

View File

@@ -5,7 +5,7 @@ Features
kern kern
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Gruppieren Informationen zu ähnlichen Funktionen Gruppiert Informationen zu ähnlichen Funktionen
---------------------------------------- ----------------------------------------
<!-- regeln --> <!-- regeln -->
- Alphabetisch sortiert - Alphabetisch sortiert
@@ -16,21 +16,21 @@ Gruppieren Informationen zu ähnlichen Funktionen
<!-- funktion --> <!-- funktion -->
Anzeigemodus umschalten Anzeigemodus umschalten
<!-- ziel --> <!-- ziel -->
Kompaktmodus zum lesen und Vollmodus zum vertiefen und anpassen Kompaktmodus zum Lesen und Vollmodus zum Vertiefen und Anpassen
<!-- regeln --> <!-- regeln -->
- Standard ist Kompaktmodus - Standard ist Kompaktmodus
- Vollmodus geht über responsive Spalten - Vollmodus geht über responsive Spalten
- Vollmodus zeigt alles - Vollmodus zeigt alles
- Kompaktmodus zeigt Ziel und Funktionstitel als Liste - Kompaktmodus zeigt Ziel und Funktionstitel als Liste
- Augen-Icon rechts oben
<!-- testen --> <!-- testen -->
- Klick auf Karte - MMB auf Karte
- Vollmodus sichtbar - Modus der Karte wechselt
- Klick auf freie Kartenfläche - Alternativ Klick auf Augen-Icon
- Kompaktmodus sichtbar
<!-- funktion --> <!-- funktion -->
Badge umschalten Badge umschalten
<!-- ziel --> <!-- ziel -->
Unterscheidet die Kern- und Optionalen Features Unterscheidet die Kern- und optionalen Features
<!-- regeln --> <!-- regeln -->
- Kernfarbe blau - Kernfarbe blau
- Optionalfarbe grün - Optionalfarbe grün

View File

@@ -5,49 +5,74 @@ Funktionen
kern kern
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Konkrete Funktionen zum Feature
---------------------------------------- ----------------------------------------
<!-- funktion -->
Testen anpassen
<!-- regeln --> <!-- regeln -->
- Ein Testen-Block je Funktion - Alphabetisch sortiert
- Leeren entfernt den Block - Anpassen nur im Vollmodus
<!-- testen --> - Verlassen speichert
- Klick auf Testen oder + Testen - Escape verwirft
- Inhalt anpassen ----------------------------------------
<!-- funktion -->
Funktion anpassen
<!-- testen -->
- Klick auf Text
- Inhalt ändern
<!-- funktion --> <!-- funktion -->
Funktion entfernen Funktion entfernen
<!-- regeln --> <!-- regeln -->
- Feld leeren entfernt auch - x-Icon
<!-- testen --> <!-- testen -->
- Hover über Funktion - Vollmodus öffnen
- Klick auf x - RMB auf Funktion
- Klick auf "Funktion entfernen"
- Bestätigen im Modal
- Funktion entfernt
<!-- funktion --> <!-- funktion -->
Funktion hinzufügen Funktion erstellen
<!-- regeln --> <!-- regeln -->
- Alphabetisch sortiert - +-Icon
- Neue Funktion rückt nach dem Speichern an ihre Position
<!-- testen --> <!-- testen -->
- Klick auf + Funktion - Vollmodus öffnen
- Inhalt eingeben - RMB auf Featurecard
<!-- funktion --> - Klick auf "Funktion erstellen"
Ziel anpassen - Namen im Modal eingeben
<!-- regeln --> - Bestätigen
- Ein Ziel je Funktion
- Leeren entfernt das Ziel
<!-- testen -->
- Klick auf Ziel oder + Ziel
- Inhalt anpassen
<!-- funktion --> <!-- funktion -->
Regeln anpassen Regeln anpassen
<!-- ziel -->
Spezifische Hinweise für die Funktion
<!-- regeln --> <!-- regeln -->
- Eine Regel je Zeile - Eine Regel je Zeile
- Leeren entfernt alle Regeln - Jeweils nur 1 kompakter Satz
<!-- testen --> <!-- testen -->
- Klick auf Regeln oder + Regeln - Vollmodus öffnen
- Klick auf Regeln der Funktion
- Inhalt anpassen
<!-- funktion -->
Testen anpassen
<!-- ziel -->
Beschreibt die Schritte zum Prüfen der Funktion
<!-- regeln -->
- Ein Schritt je Zeile
- Jeweils nur 1 kompakter Satz
<!-- testen -->
- Vollmodus öffnen
- Klick auf Testen der Funktion
- Inhalt anpassen
<!-- funktion -->
Titel anpassen
<!-- ziel -->
Beschreibt die Funktion so kompakt wie möglich
<!-- regeln -->
- Nur wenige Wörter
<!-- testen -->
- Vollmodus öffnen
- Klick auf Titel der Funktion
- Inhalt anpassen
<!-- funktion -->
Ziel anpassen
<!-- ziel -->
Beschreibt kurz das Kernziel der Funktion
<!-- regeln -->
- 1-2 simple Sätze
- Nur wenn der Titel nicht selbsterklärend ist
<!-- testen -->
- Vollmodus öffnen
- Klick auf Ziel der Funktion
- Inhalt anpassen - Inhalt anpassen

View File

@@ -11,6 +11,8 @@ Gruppiert Features für ein Projekt
- Alphabetisch sortiert - Alphabetisch sortiert
- Aktives Projekt ist hervorgehoben - Aktives Projekt ist hervorgehoben
- 1-2 Wörter für den Namen - 1-2 Wörter für den Namen
- Verlassen speichert
- Escape verwirft
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Projekt anpassen Projekt anpassen
@@ -30,7 +32,7 @@ Projekt aufrufen
<!-- funktion --> <!-- funktion -->
Projekt entfernen Projekt entfernen
<!-- regeln --> <!-- regeln -->
- X-Icon - x-Icon
- Auch Features entfernen - Auch Features entfernen
<!-- testen --> <!-- testen -->
- RMB auf Projektname - RMB auf Projektname

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { nextTick, onMounted, ref, watch } from 'vue' import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { marked } from 'marked' import { marked } from 'marked'
import { featureSpeichern } from '../api.js' import { featureSpeichern } from '../api.js'
@@ -8,7 +8,7 @@ const props = defineProps({
feature: { type: Object, required: true }, feature: { type: Object, required: true },
vollmodus: { type: Boolean, default: false }, // frisch angelegt: direkt im Vollmodus öffnen vollmodus: { type: Boolean, default: false }, // frisch angelegt: direkt im Vollmodus öffnen
}) })
const emit = defineEmits(['aktualisiert', 'kontextmenue']) const emit = defineEmits(['aktualisiert', 'entfernen'])
function kopie(sekt) { function kopie(sekt) {
return { return {
@@ -71,7 +71,6 @@ function abbrechen() {
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()
@@ -99,14 +98,56 @@ function abbrechenLeer(a) {
if (!a.original && a.feld === 'funktion') s.value.funktionen.splice(a.i, 1) if (!a.original && a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
} }
function neueFunktion() { // Kontextmenü: RMB auf eine Funktion (entfernen) oder auf die Karte (erstellen/Feature entfernen)
s.value.funktionen.unshift({ text: '', ziel: '', regeln: '', testen: '' }) const kontextMenue = ref(null) // {funktion: index|null, x, y}
starte('funktion', 0)
function oeffneKontextMenue(e, i = null) {
kontextMenue.value = { funktion: i, x: e.clientX, y: e.clientY }
} }
function entferneFunktion(i) { function schliesseKontextMenue() {
s.value.funktionen.splice(i, 1) kontextMenue.value = null
speichern() }
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
// Modal für Funktion erstellen und entfernen
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', i}
const modalName = ref('')
const modalFeld = ref(null)
async function menueFunktionErstellen() {
kontextMenue.value = null
modal.value = { art: 'erstellen' }
modalName.value = ''
await nextTick()
modalFeld.value?.focus()
}
function menueFunktionEntfernen() {
const i = kontextMenue.value.funktion
kontextMenue.value = null
modal.value = { art: 'entfernen', i }
}
function menueFeatureEntfernen() {
kontextMenue.value = null
emit('entfernen')
}
async function erstellenBestaetigen() {
const name = modalName.value.trim()
if (!name) return
s.value.funktionen.push({ text: name, ziel: '', regeln: '', testen: '' })
modal.value = null
await speichern()
}
async function entfernenBestaetigen() {
s.value.funktionen.splice(modal.value.i, 1)
modal.value = null
await speichern()
} }
function wechsleArt() { function wechsleArt() {
@@ -157,20 +198,10 @@ function punkte(text) {
return text.split('\n').map(z => z.replace(/^[-*]\s*/, '').trim()).filter(Boolean) return text.split('\n').map(z => z.replace(/^[-*]\s*/, '').trim()).filter(Boolean)
} }
// Standardansicht ist reine Übersicht: Klick öffnet die ausführliche Ansicht, // Kompaktmodus zum Lesen, Vollmodus zum Anpassen — MMB auf die Karte
// nur dort wird bearbeitet; Klick auf freie Fläche schließt sie wieder — // oder das Augen-Icon schaltet um
// außer der Klick hat gerade eine Bearbeitung beendet (Blur kommt vor dem Klick) function wechsleAnsicht() {
let blurZeit = 0 gross.value = !gross.value
function wechsleAnsicht(e) {
if (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(() => {
@@ -182,8 +213,9 @@ onMounted(() => {
<article <article
class="karte" class="karte"
:class="[s.art, { gross }]" :class="[s.art, { gross }]"
@click="wechsleAnsicht" @mousedown.middle.prevent
@contextmenu.prevent.stop="emit('kontextmenue', $event)" @auxclick.middle.prevent="wechsleAnsicht"
@contextmenu.prevent.stop="oeffneKontextMenue($event)"
> >
<header> <header>
<input <input
@@ -204,6 +236,11 @@ onMounted(() => {
@click="wechsleArt" @click="wechsleArt"
>{{ s.art }}</button> >{{ s.art }}</button>
<span v-else class="badge" :class="s.art === 'kern' ? 'blau' : 'gruen'">{{ s.art }}</span> <span v-else class="badge" :class="s.art === 'kern' ? 'blau' : 'gruen'">{{ s.art }}</span>
<button
class="icon-knopf"
:title="gross ? 'Kompaktmodus' : 'Vollmodus'"
@click="wechsleAnsicht"
>👁</button>
</header> </header>
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }"> <section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
@@ -239,13 +276,13 @@ onMounted(() => {
<section :class="{ leer: !s.funktionen.length }"> <section :class="{ leer: !s.funktionen.length }">
<p class="gruppe">Funktionen</p> <p class="gruppe">Funktionen</p>
<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"
:key="i" :key="i"
class="funktion" class="funktion"
:class="{ offen: aktiv && aktiv.i === i }" :class="{ offen: aktiv && aktiv.i === i }"
@contextmenu.prevent.stop="oeffneKontextMenue($event, i)"
> >
<div class="zeile kopf"> <div class="zeile kopf">
<span class="pfeil"></span> <span class="pfeil"></span>
@@ -258,7 +295,6 @@ 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 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) -->
@@ -320,11 +356,49 @@ onMounted(() => {
</section> </section>
<p v-if="meldung" class="meldung">{{ meldung }}</p> <p v-if="meldung" class="meldung">{{ meldung }}</p>
<div
v-if="kontextMenue"
class="kontextmenue"
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
>
<button v-if="kontextMenue.funktion !== null" @click="menueFunktionEntfernen"> Funktion entfernen</button>
<template v-else>
<button v-if="gross" @click="menueFunktionErstellen">+ Funktion erstellen</button>
<button @click="menueFeatureEntfernen"> Feature entfernen</button>
</template>
</div>
<div v-if="modal" class="schleier" @click.self="modal = null">
<div class="modal karte">
<template v-if="modal.art === 'erstellen'">
<h2>Funktion erstellen</h2>
<input
ref="modalFeld"
v-model="modalName"
placeholder="Funktionstitel"
@keyup.enter="erstellenBestaetigen"
@keydown.esc="modal = null"
>
<footer>
<button @click="modal = null">Abbrechen</button>
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
</footer>
</template>
<template v-else>
<h2>Funktion entfernen</h2>
<p class="frage">{{ s.funktionen[modal.i].text }} entfernen?</p>
<footer>
<button @click="modal = null">Abbrechen</button>
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
</footer>
</template>
</div>
</div>
</article> </article>
</template> </template>
<style scoped> <style scoped>
.karte { cursor: pointer; }
.karte.kern { border-color: var(--akzent); } .karte.kern { border-color: var(--akzent); }
.karte.optional { border-color: var(--gruen); } .karte.optional { border-color: var(--gruen); }
@@ -356,7 +430,7 @@ header {
padding-bottom: 0.55rem; padding-bottom: 0.55rem;
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; flex: 1; }
/* Im Bearbeitungsmodus zeigt Hover die klickbare Titelfläche */ /* Im Bearbeitungsmodus zeigt Hover die klickbare Titelfläche */
.karte.gross h3 { .karte.gross h3 {
padding: 0.1rem 0.3rem; padding: 0.1rem 0.3rem;
@@ -392,7 +466,7 @@ section.leer { display: none; }
padding: 0.1rem 0.3rem; padding: 0.1rem 0.3rem;
margin: 0.1rem 0 0.1rem -0.3rem; margin: 0.1rem 0 0.1rem -0.3rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: text;
} }
.hinweis:hover { background: var(--code-bg); } .hinweis:hover { background: var(--code-bg); }
.hinzu { .hinzu {
@@ -470,7 +544,10 @@ textarea {
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.5; line-height: 1.5;
} }
.klickbar { cursor: pointer; user-select: none; } /* Karte: MMB schaltet den Modus um; Texte sind nur im Vollmodus bearbeitbar */
.karte { cursor: pointer; }
.klickbar { user-select: none; }
.karte.gross .klickbar, .karte.gross h3 { cursor: text; }
.inhalt.klickbar { border-radius: 4px; } .inhalt.klickbar { border-radius: 4px; }
.inhalt.klickbar:hover { background: var(--code-bg); } .inhalt.klickbar:hover { background: var(--code-bg); }
h3, .hinweis { user-select: none; } h3, .hinweis { user-select: none; }

View File

@@ -29,15 +29,11 @@ function aktualisiert(altId, f) {
if (neuId.value === altId) neuId.value = f.id if (neuId.value === altId) neuId.value = f.id
} }
// Kontextmenü: Rechtsklick auf eine Card (entfernen) oder den Hauptbereich (erstellen) // Kontextmenü auf dem Hauptbereich (erstellen); das Menü der Card führt die Karte selbst
const kontextMenue = ref(null) // {feature|null, x, y} const kontextMenue = ref(null) // {x, y}
function oeffneKartenMenue(e, f) {
kontextMenue.value = { feature: f, x: e.clientX, y: e.clientY }
}
function oeffneBereichsMenue(e) { function oeffneBereichsMenue(e) {
kontextMenue.value = { feature: null, x: e.clientX, y: e.clientY } kontextMenue.value = { x: e.clientX, y: e.clientY }
} }
function schliesseKontextMenue() { function schliesseKontextMenue() {
@@ -61,9 +57,7 @@ async function menueErstellen() {
modalFeld.value?.focus() modalFeld.value?.focus()
} }
function menueEntfernen() { function featureEntfernen(feature) {
const feature = kontextMenue.value.feature
kontextMenue.value = null
meldung.value = '' meldung.value = ''
modal.value = { art: 'entfernen', feature } modal.value = { art: 'entfernen', feature }
} }
@@ -110,7 +104,7 @@ watch(() => props.projekt, laden, { immediate: true })
:feature="f" :feature="f"
:vollmodus="f.id === neuId" :vollmodus="f.id === neuId"
@aktualisiert="aktualisiert" @aktualisiert="aktualisiert"
@kontextmenue="oeffneKartenMenue($event, f)" @entfernen="featureEntfernen(f)"
/> />
</div> </div>
@@ -119,8 +113,7 @@ watch(() => props.projekt, laden, { immediate: true })
class="kontextmenue" class="kontextmenue"
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }" :style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
> >
<button v-if="kontextMenue.feature" @click="menueEntfernen"> Feature entfernen</button> <button @click="menueErstellen">+ Feature erstellen</button>
<button v-else @click="menueErstellen">+ Feature erstellen</button>
</div> </div>
<div v-if="modal" class="schleier" @click.self="modal = null"> <div v-if="modal" class="schleier" @click.self="modal = null">