This commit is contained in:
team3
2026-06-18 14:30:21 +02:00
parent d8932c90d6
commit 9c0f622e0c
30 changed files with 45808 additions and 217 deletions

View File

@@ -0,0 +1,211 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { fetchBausteineUebersicht } from '../api.js'
const props = defineProps({
topic: { type: String, required: true },
})
const emit = defineEmits(['close'])
const items = ref([])
const loading = ref(true)
const error = ref(null)
// Stufen-Reihenfolge + Anzeige-Label (Farbe via CSS-Klasse st-<key>)
const STUFEN = [
{ key: 'einfach', label: 'Einfach' },
{ key: 'mittel', label: 'Mittel' },
{ key: 'schwer', label: 'Schwer' },
]
watch(() => props.topic, load, { immediate: true })
async function load() {
loading.value = true
error.value = null
items.value = []
try {
items.value = await fetchBausteineUebersicht(props.topic)
} catch (e) {
error.value = 'Übersicht nicht verfügbar — bitte erst Bausteine erstellen.'
} finally {
loading.value = false
}
}
// Nur nicht-leere Stufen-Gruppen je Baustein (v-if + v-for nicht auf einem Element)
function gruppen(b) {
return STUFEN
.map((st) => ({ ...st, subs: (b.subbausteine || []).filter((s) => s.stufe === st.key) }))
.filter((g) => g.subs.length)
}
const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine?.length || 0), 0))
</script>
<template>
<div class="bk-view">
<header class="bk-head">
<h1>{{ topic }}</h1>
<span class="bk-sub">Bausteine-Übersicht</span>
<span v-if="items.length" class="bk-count">{{ items.length }} Bausteine · {{ subTotal }} Subbausteine</span>
<span class="bk-spacer"></span>
<button class="bk-close" title="Schließen" @click="emit('close')"></button>
</header>
<div v-if="loading" class="bk-empty-state">Lade</div>
<div v-else-if="error" class="bk-empty-state">{{ error }}</div>
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
<div v-else class="bk-grid">
<article v-for="b in items" :key="b.num" class="bk-card">
<h3 class="bk-title"><span class="bk-num">{{ b.num }}</span>{{ b.titel }}</h3>
<p v-if="b.beschreibung" class="bk-desc">{{ b.beschreibung }}</p>
<div v-if="b.subbausteine && b.subbausteine.length" class="bk-stufen">
<div v-for="g in gruppen(b)" :key="g.key" class="bk-stufe" :class="'st-' + g.key">
<span class="bk-stufe-label">{{ g.label }}</span>
<ul>
<li v-for="s in g.subs" :key="s.titel" :class="{ rand: s.relevanz === 'rand' }">
{{ s.titel }}<span v-if="s.relevanz === 'rand'" class="rand-tag" title="Randthema — nur im FullGuide">Rand</span>
</li>
</ul>
</div>
</div>
<p v-else class="bk-no-subs">Keine Subbausteine.</p>
</article>
</div>
</div>
</template>
<style scoped>
.bk-view {
flex: 1;
min-width: 0;
height: 100dvh;
display: flex;
flex-direction: column;
background: var(--bg-preview);
}
.bk-head {
display: flex;
align-items: baseline;
gap: 0.75rem;
padding: 1.25rem 2rem;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
.bk-head h1 { font-size: 1.5rem; }
.bk-sub { color: var(--text-faint); font-size: 0.9rem; font-weight: 600; }
.bk-count { color: var(--text-muted); font-size: 0.82rem; }
.bk-spacer { flex: 1; }
.bk-close {
align-self: center;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--panel);
color: var(--text);
width: 2rem;
height: 2rem;
cursor: pointer;
}
.bk-close:hover { border-color: var(--accent); }
.bk-empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.bk-grid {
flex: 1;
overflow-y: auto;
padding: 1.5rem 2rem 4rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1rem;
align-content: start;
}
.bk-card {
background: var(--panel);
border: 1px solid var(--border);
border-top: 3px solid var(--accent-border);
border-radius: 10px;
padding: 1rem 1.1rem;
}
.bk-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 1rem;
margin-bottom: 0.4rem;
}
.bk-num {
flex: 0 0 auto;
min-width: 26px;
height: 26px;
padding: 0 6px;
border-radius: 7px;
background: var(--accent);
color: var(--on-accent);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.78rem;
font-weight: 700;
}
.bk-desc {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.45;
margin-bottom: 0.7rem;
}
.bk-stufen { display: flex; flex-direction: column; gap: 0.55rem; }
.bk-stufe {
border-left: 3px solid var(--st);
padding-left: 0.6rem;
}
.bk-stufe.st-einfach { --st: var(--success-border); }
.bk-stufe.st-mittel { --st: var(--warning-border); }
.bk-stufe.st-schwer { --st: var(--danger); }
.bk-stufe-label {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--st);
}
.bk-stufe ul {
list-style: none;
margin: 0.25rem 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.bk-stufe li {
font-size: 0.85rem;
color: var(--text);
line-height: 1.35;
}
.bk-stufe li.rand { color: var(--text-faint); }
.rand-tag {
margin-left: 0.4rem;
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-faint);
border: 1px solid var(--border-strong);
border-radius: 4px;
padding: 0 4px;
}
.bk-no-subs { color: var(--text-faint); font-size: 0.8rem; font-style: italic; }
</style>

View File

@@ -1,10 +1,10 @@
<script setup>
import { ref, computed } from 'vue'
import { useConfirm } from '../composables/useConfirm.js'
import { fetchQuelle } from '../api.js'
const props = defineProps({
topics: { type: Array, required: true },
projects: { type: Array, default: () => [] },
selectedTopic: { type: String, default: null },
stats: { type: Object, default: null },
fortschritt: { type: Object, default: () => ({}) },
@@ -20,9 +20,17 @@ const props = defineProps({
dark: { type: Boolean, default: false },
provider: { type: String, default: 'claude' },
providers: { type: Array, default: () => [] },
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
})
const emit = defineEmits(['select', 'create', 'createThema', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
// Accordion: höchstens ein Panel offen. IDs: 'bausteine', 'fmt-<Format>', 'topic-<Name>'.
const openPanel = ref(null)
const isOpen = (id) => openPanel.value === id
function togglePanel(id) {
openPanel.value = openPanel.value === id ? null : id
}
function providerAvailable(id) {
const p = props.providers.find((x) => x.id === id)
@@ -40,6 +48,7 @@ const trackerItems = computed(() => {
{ label: 'Themen', value: String(props.stats.themen ?? 0), title: 'Themen inkl. Projekte' },
{ label: 'MiniGuides', value: fmt('MiniGuide'), title: 'absolviert/erstellt' },
{ label: 'Guides', value: fmt('Guide'), title: 'absolviert/erstellt' },
{ label: 'ProGuides', value: fmt('ProGuide'), title: 'absolviert/erstellt' },
{ label: 'FullGuides', value: fmt('FullGuide'), title: 'absolviert/erstellt' },
]
})
@@ -48,6 +57,7 @@ const formats = [
{ key: 'OnePager', label: 'OnePager' },
{ key: 'MiniGuide', label: 'MiniGuide' },
{ key: 'Guide', label: 'Guide' },
{ key: 'ProGuide', label: 'ProGuide' },
{ key: 'FullGuide', label: 'FullGuide' },
]
@@ -82,6 +92,12 @@ function handleBausteinePlay() {
emit('bausteineClick', { instructions: '' })
}
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
function onBausteineName() {
if (props.bausteine.ready && !props.bausteine.generating) emit('openBausteineView')
else togglePanel('bausteine')
}
function guideStatus(format) {
// Laufende Generierung hat Vorrang — sonst maskiert ein älterer fertiger
// Guide den Lauf und ▶ würde Duplikate starten.
@@ -132,11 +148,11 @@ function abgebrochen(format) {
return latest?.status === 'error' && (latest.error_msg || '').startsWith('Abgebrochen')
}
// Name-Klick: fertiger Guide → Vorschau, sonst Aktions-Panel auf/zu.
function handleFormatClick(format) {
const guide = props.doneByFormat[format]
if (guide) {
emit('preview', guide)
}
if (guide) emit('preview', guide)
else togglePanel('fmt-' + format)
}
// Sperr-Gründe kommen vom Backend (GET /guides/locks) — die Regeln existieren
@@ -179,13 +195,6 @@ function handleDelete(format) {
// Erstellen-Bereich: inline aufklappbar (Name + weitere Infos + Quellen-Typ).
const dlg = ref(false)
const form = ref({ name: '', instructions: '', sourceType: 'thema', sourceOrt: '' })
const SOURCE_HINTS = {
thema: 'Web-Recherche zum Thema.',
link: 'Seite wird gecrawlt (gleiche Domain, begrenzt) — Klausur-Fokus.',
projekt: 'Ordner wird gelesen — Architektur/Features verstehen.',
uni: 'Ordner wird gelesen — Klausur-Vorbereitung.',
}
const sourceHint = computed(() => SOURCE_HINTS[form.value.sourceType])
const canCreate = computed(() => {
if (!form.value.name.trim()) return false
if (['link', 'projekt', 'uni'].includes(form.value.sourceType)) return !!form.value.sourceOrt.trim()
@@ -212,8 +221,45 @@ function confirmDeleteTopic(topic) {
armOrRun('topic-' + topic, () => emit('deleteTopic', topic))
}
function confirmDeleteProject(name) {
armOrRun('project-' + name, () => emit('deleteProject', name))
// --- Thema-Edit: Klick auf ein Thema wählt es UND klappt die Quellen-Form auf ---
const editTopic = ref(null)
const editForm = ref({ type: 'thema', ort: '', spec: '' })
const editLoading = ref(false)
const canSave = computed(() => {
if (['link', 'projekt', 'uni'].includes(editForm.value.type)) return !!editForm.value.ort.trim()
return true
})
async function onTopicClick(t) {
emit('select', t)
if (openPanel.value === 'topic-' + t) { openPanel.value = null; return }
openPanel.value = 'topic-' + t
editTopic.value = t
editLoading.value = true
try {
const q = await fetchQuelle(t)
editForm.value = { type: q.type || 'thema', ort: q.ort || '', spec: q.spec || '' }
} catch {
editForm.value = { type: 'thema', ort: '', spec: '' }
} finally {
editLoading.value = false
}
}
function setEditType(t) {
editForm.value.type = t
editForm.value.ort = ''
}
function saveQuelle() {
if (!canSave.value || !editTopic.value) return
emit('updateQuelle', {
topic: editTopic.value,
type: editForm.value.type,
ort: editForm.value.ort.trim(),
spec: editForm.value.spec.trim(),
})
openPanel.value = null
}
</script>
@@ -242,7 +288,7 @@ function confirmDeleteProject(name) {
<!-- Erstellen: inline aufklappbar (kein Modal) -->
<div v-if="dlg" class="thema-panel">
<input class="dlg-input" v-model="form.name" placeholder="Thema-Name…" @keyup.enter="createThema" autofocus />
<textarea class="dlg-textarea" v-model="form.instructions" rows="3" placeholder="Weitere Informationen / Spezifikation (optional)…"></textarea>
<textarea class="dlg-textarea" v-model="form.instructions" rows="2" placeholder="Weitere Infos (optional)…"></textarea>
<div class="dlg-sources">
<button :class="{ active: form.sourceType === 'thema' }" @click="form.sourceType = 'thema'; form.sourceOrt = ''">Thema</button>
<button :class="{ active: form.sourceType === 'link' }" @click="form.sourceType = 'link'; form.sourceOrt = ''">Link</button>
@@ -254,12 +300,13 @@ function confirmDeleteProject(name) {
class="dlg-input" v-model="form.sourceOrt"
placeholder="https://…" @keyup.enter="createThema"
/>
<input
<select
v-else-if="form.sourceType === 'projekt' || form.sourceType === 'uni'"
class="dlg-input" v-model="form.sourceOrt"
placeholder="Ordner ab ./ (z.B. projects/meinprojekt)" @keyup.enter="createThema"
/>
<p class="dlg-hint">{{ sourceHint }}</p>
>
<option value="" disabled>Ordner wählen</option>
<option v-for="fo in (folders[form.sourceType] || [])" :key="fo.ort" :value="fo.ort">{{ fo.name }}</option>
</select>
<div class="dlg-actions">
<button class="dlg-cancel" @click="dlg = false">Abbrechen</button>
<button class="dlg-create" :disabled="!canCreate" @click="createThema">Erstellen</button>
@@ -283,66 +330,60 @@ function confirmDeleteProject(name) {
<div class="progress-info" v-if="activeGenerations.length">
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
</div>
<div
class="format-row bausteine-row ord-bausteine"
:class="{ 'is-active': bausteineState === 'generating' || bausteine.partial }"
>
<div class="format-name bausteine-name">
<span class="format-label">Bausteine</span>
<span
v-if="bausteine.partial && bausteineState !== 'generating'"
class="resume-badge"
title="Abgebrochen — ▶ setzt fort"
>Pausiert</span>
<span class="step-dots">
<div class="ord-bausteine">
<div
class="format-row bausteine-row"
:class="{ 'is-active': bausteineState === 'generating' || bausteine.partial, 'row-open': isOpen('bausteine') }"
>
<button class="format-name bausteine-name" @click="onBausteineName">
<span class="format-label">Bausteine</span>
<span
v-for="s in (bausteine.steps || [])"
:key="s.label"
class="step-dot"
:class="s.state"
:title="s.state === 'active' ? (bausteine.progress || s.label) : s.label"
></span>
</span>
<span
v-if="bausteineState === 'generating'"
class="format-x"
:class="{ armed: pendingConfirm === 'bausteine' }"
title="Aktuellen Schritt abbrechen (Fortschritt bleibt)"
@click.stop="confirmCancelBausteine"
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
<span
v-else-if="bausteine.partial"
class="format-x"
:class="{ armed: pendingConfirm === 'bausteine' }"
title="Fortschritt löschen"
@click.stop="confirmResetBausteine"
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
v-if="bausteine.partial && bausteineState !== 'generating'"
class="resume-badge"
title="Abgebrochen — Fortsetzen möglich"
>Pausiert</span>
<span class="step-dots">
<span
v-for="s in (bausteine.steps || [])"
:key="s.label"
class="step-dot"
:class="s.state"
:title="s.state === 'active' ? (bausteine.progress || s.label) : s.label"
></span>
</span>
</button>
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')"></button>
</div>
<div class="format-actions">
<template v-if="bausteineState !== 'generating'">
<div v-if="isOpen('bausteine')" class="action-panel">
<template v-if="bausteineState === 'generating'">
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
</template>
<template v-else>
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Neu generieren' : 'Generieren' }}</button>
<button
class="action-btn play"
:title="bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Bausteine neu erstellen' : 'Bausteine erstellen'"
@click="handleBausteinePlay"
></button>
v-if="bausteine.ready || bausteine.partial"
class="panel-btn danger"
:class="{ armed: pendingConfirm === 'bausteine' }"
@click="confirmResetBausteine"
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Entfernen' }}</button>
</template>
</div>
</div>
<div v-if="bausteineState === 'generating'" class="format-progress ord-bausteine">
{{ bausteine.progress || 'Wartend' }}
</div>
<div v-if="bausteine.error && !bausteine.error.startsWith('Abgebrochen')" class="format-error ord-bausteine">
<span class="format-error-text">{{ bausteine.error }}</span>
<div v-if="bausteineState === 'generating'" class="format-progress">
{{ bausteine.progress || 'Wartend' }}
</div>
<div v-if="bausteine.error && !bausteine.error.startsWith('Abgebrochen')" class="format-error">
<span class="format-error-text">{{ bausteine.error }}</span>
</div>
</div>
<!-- OnePager (unabhängig von Bausteinen) steht per CSS-order vor der Bausteine-Zeile -->
<div v-for="f in formats" :key="f.key" :style="{ order: f.key === 'OnePager' ? 1 : 3 }">
<div :class="['format-row', 'fmt-' + guideStatus(f.key), { 'fmt-paused': abgebrochen(f.key) }]">
<div :class="['format-row', 'fmt-' + guideStatus(f.key), { 'fmt-paused': abgebrochen(f.key), 'row-open': isOpen('fmt-' + f.key) }]">
<button class="format-name" @click="handleFormatClick(f.key)">
<span class="format-label">{{ f.label }}</span>
<span
v-if="abgebrochen(f.key)"
class="resume-badge"
title="Abgebrochen — ▶ setzt fort"
title="Abgebrochen — Fortsetzen möglich"
>Pausiert</span>
<span class="step-dots" v-if="guideSteps(f.key).length">
<span
@@ -353,25 +394,27 @@ function confirmDeleteProject(name) {
:title="s.state === 'active' ? (latestByFormat[f.key]?.progress || s.label) : s.label"
></span>
</span>
<span
v-if="guideStatus(f.key) !== 'none' || abgebrochen(f.key)"
class="format-x"
:class="{ armed: pendingConfirm === 'fmt-' + f.key }"
@click.stop="handleDelete(f.key)"
:title="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued' ? 'Abbrechen'
: abgebrochen(f.key) ? 'Fortschritt löschen' : 'Löschen'"
>{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : '×' }}</span>
</button>
<div class="format-actions">
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
<button
class="action-btn play"
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
:disabled="!!playLock(f.key)"
@click="handlePlay(f.key)"
></button>
</template>
</div>
<button class="panel-toggle" :class="{ open: isOpen('fmt-' + f.key) }" title="Aktionen" @click.stop="togglePanel('fmt-' + f.key)"></button>
</div>
<div v-if="isOpen('fmt-' + f.key)" class="action-panel">
<template v-if="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued'">
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'fmt-' + f.key }" @click="handleDelete(f.key)">{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : 'Abbrechen' }}</button>
</template>
<template v-else>
<button
class="panel-btn play"
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
:disabled="!!playLock(f.key)"
@click="handlePlay(f.key)"
>{{ abgebrochen(f.key) ? 'Fortsetzen' : guideStatus(f.key) === 'done' ? 'Neu generieren' : 'Generieren' }}</button>
<button
v-if="guideStatus(f.key) !== 'none' || abgebrochen(f.key)"
class="panel-btn danger"
:class="{ armed: pendingConfirm === 'fmt-' + f.key }"
@click="handleDelete(f.key)"
>{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : abgebrochen(f.key) ? 'Fortschritt löschen' : 'Entfernen' }}</button>
</template>
</div>
<div
v-if="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued'"
@@ -393,34 +436,45 @@ function confirmDeleteProject(name) {
<li
v-for="t in topics"
:key="t"
:class="{ active: t === selectedTopic }"
@click="emit('select', t)"
:class="{ active: t === selectedTopic, 'li-open': isOpen('topic-' + t) }"
>
<span>{{ t }}</span>
<button
class="delete-topic"
:class="{ armed: pendingConfirm === 'topic-' + t }"
@click.stop="confirmDeleteTopic(t)"
title="Thema und alle Guides löschen"
>{{ pendingConfirm === 'topic-' + t ? 'Sicher?' : '×' }}</button>
<div class="topic-row" @click="onTopicClick(t)">
<span class="topic-name">{{ t }}</span>
<span class="panel-toggle" :class="{ open: isOpen('topic-' + t) }"></span>
</div>
<div v-if="isOpen('topic-' + t)" class="thema-panel edit-panel" @click.stop>
<p v-if="editLoading" class="dlg-hint">Lade</p>
<template v-else>
<textarea class="dlg-textarea" v-model="editForm.spec" rows="2" placeholder="Weitere Infos (optional)…"></textarea>
<div class="dlg-sources">
<button :class="{ active: editForm.type === 'thema' }" @click="setEditType('thema')">Thema</button>
<button :class="{ active: editForm.type === 'link' }" @click="setEditType('link')">Link</button>
<button :class="{ active: editForm.type === 'projekt' }" @click="setEditType('projekt')">Projekt</button>
<button :class="{ active: editForm.type === 'uni' }" @click="setEditType('uni')">Uni</button>
</div>
<input
v-if="editForm.type === 'link'"
class="dlg-input" v-model="editForm.ort"
placeholder="https://…"
/>
<select
v-else-if="editForm.type === 'projekt' || editForm.type === 'uni'"
class="dlg-input" v-model="editForm.ort"
>
<option value="" disabled>Ordner wählen</option>
<option v-for="fo in (folders[editForm.type] || [])" :key="fo.ort" :value="fo.ort">{{ fo.name }}</option>
</select>
<div class="dlg-actions">
<button
class="dlg-delete"
:class="{ armed: pendingConfirm === 'topic-' + t }"
@click="confirmDeleteTopic(t)"
>{{ pendingConfirm === 'topic-' + t ? 'Sicher?' : 'Löschen' }}</button>
<button class="dlg-create" :disabled="!canSave" @click="saveQuelle">Aktualisieren</button>
</div>
</template>
</div>
</li>
<template v-if="projects.length">
<li class="projects-divider">Projekte</li>
<li
v-for="p in projects"
:key="'project-' + p"
:class="{ active: p === selectedTopic, 'project-item': true }"
@click="emit('select', p)"
>
<span>{{ p }}</span>
<button
class="delete-topic"
:class="{ armed: pendingConfirm === 'project-' + p }"
@click.stop="confirmDeleteProject(p)"
title="Projekt entfernen (löscht ./projects-Ordner)"
>{{ pendingConfirm === 'project-' + p ? 'Sicher?' : '×' }}</button>
</li>
</template>
</ul>
</aside>
</template>
@@ -582,59 +636,35 @@ function confirmDeleteProject(name) {
}
.topic-list li {
padding: 0.6rem 1rem;
cursor: pointer;
font-size: 0.9rem;
color: var(--text);
transition: background 0.15s;
display: flex;
justify-content: space-between;
align-items: center;
}
.topic-list li:hover {
.topic-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 0.6rem 1rem;
cursor: pointer;
transition: background 0.15s;
}
.topic-row:hover {
background: var(--accent-soft);
}
.topic-list li.active {
.topic-list li.active .topic-row {
background: var(--accent-soft);
color: var(--accent-hover);
font-weight: 600;
}
.delete-topic {
display: none;
background: none;
border: none;
color: var(--danger);
font-size: 1.1rem;
cursor: pointer;
padding: 0 2px;
line-height: 1;
}
.topic-list li:hover .delete-topic {
display: block;
}
.projects-divider {
cursor: default;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-faint);
font-weight: 700;
padding: 0.6rem 1rem 0.3rem;
margin-top: 0.4rem;
border-top: 1px solid var(--border);
}
.projects-divider:hover {
background: none;
}
.topic-list li.project-item span::before {
content: '📁 ';
.topic-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Format section */
@@ -642,7 +672,7 @@ function confirmDeleteProject(name) {
display: flex;
align-items: center;
gap: 8px;
cursor: default;
cursor: pointer;
}
.step-dots {
@@ -756,13 +786,14 @@ function confirmDeleteProject(name) {
.format-name {
flex: 1;
min-width: 0;
background: none;
border: none;
text-align: left;
font-size: 0.85rem;
padding: 4px 8px;
border-radius: 4px;
cursor: default;
cursor: pointer;
color: var(--text-faint);
display: flex;
align-items: center;
@@ -770,6 +801,55 @@ function confirmDeleteProject(name) {
gap: 8px;
}
/* Accordion: Pfeil-Toggle + aufklappendes Aktions-Panel */
.panel-toggle {
flex: 0 0 auto;
background: none;
border: none;
cursor: pointer;
color: var(--text-faint);
font-size: 0.8rem;
line-height: 1;
padding: 4px 8px;
transition: transform 0.15s, color 0.15s;
}
.panel-toggle.open { transform: rotate(180deg); color: var(--text); }
.panel-toggle:hover { color: var(--accent); }
.format-row.row-open,
.topic-list li.li-open .topic-row { background: var(--panel-soft); }
.action-panel {
display: flex;
gap: 0.5rem;
padding: 0.15rem 0.75rem 0.55rem calc(0.75rem + 8px);
}
.panel-btn {
flex: 1;
padding: 0.45rem 0.6rem;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--panel);
color: var(--text);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.panel-btn:hover { border-color: var(--accent); }
.panel-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.panel-btn:disabled:hover { border-color: var(--border-strong); }
.panel-btn.play {
color: var(--success);
background: var(--success-soft);
border-color: var(--success-border);
}
.panel-btn.play:hover { background: var(--success-soft-hover); }
.panel-btn.danger { color: var(--danger); }
.panel-btn.danger:hover { border-color: var(--danger); }
.panel-btn.armed { background: var(--danger); color: #fff; border-color: var(--danger); }
.format-x {
display: none;
color: var(--danger);
@@ -915,20 +995,20 @@ function confirmDeleteProject(name) {
.new-topic-toggle:hover { background: var(--accent-hover); }
.new-topic-toggle.active { background: var(--accent-hover); }
.thema-panel {
margin: 0.5rem 0;
padding: 0.75rem;
margin: 0.4rem 0;
padding: 0.5rem;
background: var(--panel-soft);
border: 1px solid var(--border);
border-radius: 10px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 0.55rem;
gap: 0.35rem;
font-size: 0.85rem;
}
.dlg-input, .dlg-textarea {
width: 100%;
box-sizing: border-box;
padding: 0.5rem 0.6rem;
padding: 0.4rem 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
@@ -936,23 +1016,23 @@ function confirmDeleteProject(name) {
font: inherit;
}
.dlg-input:focus, .dlg-textarea:focus { outline: none; border-color: var(--accent); }
.dlg-textarea { resize: vertical; min-height: 3rem; }
.dlg-sources { display: flex; gap: 0.4rem; }
.dlg-textarea { resize: vertical; min-height: 2rem; }
.dlg-sources { display: flex; gap: 0.3rem; }
.dlg-sources button {
flex: 1;
padding: 0.45rem 0.2rem;
padding: 0.35rem 0.2rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel-soft);
color: var(--text);
cursor: pointer;
font-size: 0.82rem;
font-size: 0.8rem;
}
.dlg-sources button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.dlg-hint { margin: 0; font-size: 0.75rem; color: var(--text-faint); }
.dlg-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.3rem; }
.dlg-actions { display: flex; justify-content: flex-end; gap: 0.4rem; margin-top: 0.1rem; }
.dlg-actions button {
padding: 0.45rem 0.9rem;
padding: 0.4rem 0.8rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel-soft);
@@ -962,4 +1042,10 @@ function confirmDeleteProject(name) {
.dlg-create { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.dlg-create:disabled { opacity: 0.45; cursor: default; }
/* Thema-Edit-Panel innerhalb der Themen-Liste */
.edit-panel { margin: 0 0.75rem 0.5rem; }
.dlg-delete { margin-right: auto; color: var(--danger); }
.dlg-delete.armed { background: var(--danger); border-color: var(--danger); color: #fff; font-weight: 700; }
select.dlg-input { cursor: pointer; }
</style>