1173 lines
37 KiB
Vue
1173 lines
37 KiB
Vue
<script setup>
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { useConfirm } from '../composables/useConfirm.js'
|
||
import { fetchQuelle } from '../api.js'
|
||
|
||
const props = defineProps({
|
||
topics: { type: Array, required: true },
|
||
selectedTopic: { type: String, default: null },
|
||
stats: { type: Object, default: null },
|
||
fortschritt: { type: Object, default: () => ({}) },
|
||
locks: { type: Object, default: () => ({}) },
|
||
guideStepsDone: { type: Object, default: () => ({}) }, // höchster fertiger Schritt je Format (-1 = keiner)
|
||
uiError: { type: String, default: null },
|
||
doneByFormat: { type: Object, default: () => ({}) },
|
||
latestByFormat: { type: Object, default: () => ({}) },
|
||
allGuides: { type: Array, default: () => [] },
|
||
dismissedErrors: { type: Object, default: () => new Set() },
|
||
bausteine: { type: Object, default: () => ({ ready: false, generating: false, progress: null, error: null }) },
|
||
activeBausteine: { type: Array, default: () => [] },
|
||
pinned: { type: Boolean, default: true },
|
||
dark: { type: Boolean, default: false },
|
||
provider: { type: String, default: 'claude' },
|
||
providers: { type: Array, default: () => [] },
|
||
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
|
||
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||
stufeAnsicht: { type: Number, default: 4 }, // 1=A · 2=F · 3=E · 4=V
|
||
})
|
||
|
||
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setAnsicht', 'setStufe', 'generalExam', '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)
|
||
return p ? p.available : true
|
||
}
|
||
|
||
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', lokal: 'Lokal' }
|
||
|
||
// Tracker oben in der Navigation: Themen gesamt, pro Format erstellt/absolviert
|
||
const trackerItems = computed(() => {
|
||
if (!props.stats) return []
|
||
const f = props.stats.formate || {}
|
||
const fmt = (k) => `${f[k]?.absolviert ?? 0}/${f[k]?.erstellt ?? 0}`
|
||
return [
|
||
{ label: 'Themen', value: String(props.stats.themen ?? 0), title: 'Themen inkl. Projekte' },
|
||
{ label: 'Guides', value: fmt('Guide'), title: 'absolviert/erstellt' },
|
||
]
|
||
})
|
||
|
||
const formats = [
|
||
{ key: 'Guide', label: 'Guide' },
|
||
]
|
||
|
||
// Stufen-Ansichten des EINEN Guides (gefiltert nach Subbaustein-Ebene).
|
||
const STUFEN_ANSICHT = [
|
||
{ k: 1, label: 'A', titel: 'Anfänger' },
|
||
{ k: 2, label: 'F', titel: 'Anfänger + Fortgeschritten' },
|
||
{ k: 3, label: 'E', titel: 'bis Experte' },
|
||
{ k: 4, label: 'V', titel: 'Vollständig (inkl. Rand)' },
|
||
]
|
||
|
||
const bausteineState = computed(() => {
|
||
if (props.bausteine.generating) return 'generating'
|
||
return props.bausteine.ready ? 'done' : 'none'
|
||
})
|
||
|
||
// Re-Run ab gewählter Phase (1-basiert). null = ab Anfang / Fortsetzen.
|
||
const gewaehltePhase = ref(null)
|
||
// Phasen anklickbar, sobald etwas (teil-)gebaut ist und gerade nicht generiert wird.
|
||
const phasenWaehlbar = computed(() => (props.bausteine.ready || props.bausteine.partial) && !props.bausteine.generating)
|
||
const gewaehltesLabel = computed(() => (props.bausteine.steps || [])[(gewaehltePhase.value || 0) - 1]?.label || '')
|
||
|
||
function phaseKlick(n) {
|
||
if (!phasenWaehlbar.value) return
|
||
gewaehltePhase.value = gewaehltePhase.value === n ? null : n
|
||
}
|
||
|
||
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
||
const activeGenerations = computed(() => {
|
||
const bausteinLines = props.activeBausteine
|
||
.filter((b) => b.topic !== props.selectedTopic)
|
||
.map((b) => `${b.topic} – Bausteine: ${b.progress || 'Wartend…'}`)
|
||
const guideLines = props.allGuides
|
||
.filter((g) => (g.status === 'generating' || g.status === 'queued') && g.topic !== props.selectedTopic)
|
||
.map((g) => `${g.topic} – ${g.format}: ${g.progress || 'Wartend…'}`)
|
||
return [...bausteinLines, ...guideLines]
|
||
})
|
||
|
||
const { pending: pendingConfirm, armOrRun } = useConfirm()
|
||
|
||
function confirmCancelBausteine() {
|
||
armOrRun('bausteine', () => emit('cancelBausteine'))
|
||
}
|
||
|
||
function confirmResetBausteine() {
|
||
armOrRun('bausteine', () => emit('resetBausteine'))
|
||
}
|
||
|
||
function handleBausteinePlay() {
|
||
if (bausteineState.value === 'generating') return
|
||
// Nur „Neu generieren" (ready) nutzt eine Phase; Fortsetzen/Erstbau resumen ohne Löschen.
|
||
const abPhase = props.bausteine.ready ? (gewaehltePhase.value ?? 1) : null
|
||
emit('bausteineClick', { instructions: '', abPhase })
|
||
gewaehltePhase.value = null
|
||
}
|
||
|
||
// 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.
|
||
const latest = props.latestByFormat[format]
|
||
if (latest && (latest.status === 'generating' || latest.status === 'queued')) return latest.status
|
||
if (props.doneByFormat[format]) return 'done'
|
||
if (!latest || latest.status === 'error') return 'none'
|
||
return latest.status
|
||
}
|
||
|
||
// Schritt-Kugeln der Guide-Pipeline
|
||
const GUIDE_STEPS = ['Gliederung', 'Inhalte', 'Inhalts-Check', 'Schreiben', 'Lese-Prüfung']
|
||
|
||
// Kugeln aus dem artefakt-basierten „fertig"-Marker (wie Bausteine, nicht aus dem DB-Zähler):
|
||
// ≤ fertig = done. Läuft gerade → der nächste Schritt (fertig+1) ist aktiv.
|
||
function guideSteps(format) {
|
||
const labels = GUIDE_STEPS
|
||
const fertig = props.guideStepsDone[format] ?? -1
|
||
const st = guideStatus(format)
|
||
const aktiv = st === 'generating' || st === 'queued' ? fertig + 1 : -1
|
||
return labels.map((label, i) => ({
|
||
label,
|
||
state: i <= fertig ? 'done' : i === aktiv ? 'active' : 'pending',
|
||
}))
|
||
}
|
||
|
||
// Re-Run ab Guide-Schritt (1-basierte Kugel je Format). null = voll/Resume.
|
||
const gewaehlterStep = reactive({})
|
||
// Kugeln klickbar, sobald Artefakte existieren (Marker ≥ 0 oder fertig) und nicht generiert wird.
|
||
function guideWaehlbar(format) {
|
||
const st = guideStatus(format)
|
||
if (st === 'generating' || st === 'queued') return false
|
||
return (props.guideStepsDone[format] ?? -1) >= 0 || st === 'done'
|
||
}
|
||
function guideStepKlick(format, n) {
|
||
if (!guideWaehlbar(format)) return
|
||
gewaehlterStep[format] = gewaehlterStep[format] === n ? null : n
|
||
}
|
||
function gewaehltesStepLabel(format) {
|
||
return GUIDE_STEPS[(gewaehlterStep[format] || 0) - 1] || ''
|
||
}
|
||
|
||
function errorMsg(format) {
|
||
const latest = props.latestByFormat[format]
|
||
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
|
||
if (abgebrochen(format)) return '' // kein roter Fehler — das Pausiert-Badge zeigt den Zustand
|
||
return latest.error_msg || 'Fehler bei der Generierung'
|
||
}
|
||
|
||
// Abgebrochener Lauf = Teilfortschritt vorhanden: ▶ setzt fort, ✕ löscht den Fortschritt
|
||
function abgebrochen(format) {
|
||
const latest = props.latestByFormat[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)
|
||
else togglePanel('fmt-' + format)
|
||
}
|
||
|
||
// Sperr-Gründe kommen vom Backend (GET /guides/locks) — die Regeln existieren
|
||
// nur noch dort. Solange locks noch nicht geladen sind: Button frei, das
|
||
// Backend weist ungültige Starts ohnehin ab (sichtbar über uiError).
|
||
function playLock(format) {
|
||
return props.locks?.[format] ?? null
|
||
}
|
||
|
||
function handlePlay(format) {
|
||
if (playLock(format)) return
|
||
// Gewählte Kugel (1-basiert) → ab_step (0-basiert). Nur bei (teil-)gebautem Guide.
|
||
const abStep = guideWaehlbar(format) && gewaehlterStep[format] ? gewaehlterStep[format] - 1 : null
|
||
emit('formatClick', { format, instructions: '', abStep })
|
||
gewaehlterStep[format] = null
|
||
}
|
||
|
||
// Flash-Message-Verhalten: × blendet nur aus, nichts wird gelöscht
|
||
function dismissError(format) {
|
||
const latest = props.latestByFormat[format]
|
||
if (latest?.status === 'error') emit('dismissError', latest.id)
|
||
}
|
||
|
||
function handleDelete(format) {
|
||
if (!props.latestByFormat[format]) return
|
||
armOrRun('fmt-' + format, () => {
|
||
// Alle laufenden Generierungen des Formats abbrechen (deckt auch Duplikate ab)
|
||
const running = props.allGuides.filter(
|
||
(g) => g.topic === props.selectedTopic && g.format === format
|
||
&& (g.status === 'generating' || g.status === 'queued'),
|
||
)
|
||
if (running.length) {
|
||
for (const g of running) emit('cancelGuide', g.id)
|
||
} else if (abgebrochen(format)) {
|
||
// Pausierter Lauf: Teilfortschritt samt Schritt-Dateien löschen (Reset)
|
||
emit('deleteGuide', props.latestByFormat[format].id, true)
|
||
} else {
|
||
emit('deleteGuide', props.latestByFormat[format].id)
|
||
}
|
||
})
|
||
}
|
||
|
||
// Erstellen-Bereich: inline aufklappbar (Name + weitere Infos + Quellen-Typ).
|
||
const dlg = ref(false)
|
||
const form = ref({ name: '', instructions: '', sourceType: 'thema', sourceOrt: '' })
|
||
const canCreate = computed(() => {
|
||
if (!form.value.name.trim()) return false
|
||
if (['link', 'projekt', 'uni'].includes(form.value.sourceType)) return !!form.value.sourceOrt.trim()
|
||
return true
|
||
})
|
||
|
||
function toggleErstellen() {
|
||
if (!dlg.value) form.value = { name: '', instructions: '', sourceType: 'thema', sourceOrt: '' }
|
||
dlg.value = !dlg.value
|
||
}
|
||
|
||
function createThema() {
|
||
if (!canCreate.value) return
|
||
emit('createThema', {
|
||
topic: form.value.name.trim(),
|
||
instructions: form.value.instructions.trim(),
|
||
sourceType: form.value.sourceType,
|
||
sourceOrt: form.value.sourceOrt.trim(),
|
||
})
|
||
dlg.value = false
|
||
}
|
||
|
||
function confirmDeleteTopic(topic) {
|
||
armOrRun('topic-' + topic, () => emit('deleteTopic', topic))
|
||
}
|
||
|
||
// --- Thema-Edit: Name-Klick wählt nur; das Chevron klappt die Quellen-Form auf/zu (Default zu) ---
|
||
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 toggleTopicPanel(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>
|
||
|
||
<template>
|
||
<aside class="sidebar" @mouseleave="emit('sidebarLeave')">
|
||
<div class="stats-bar" v-if="trackerItems.length">
|
||
<div class="stat" v-for="item in trackerItems" :key="item.label" :title="item.title">
|
||
<span class="stat-value">{{ item.value }}</span>
|
||
<span class="stat-label">{{ item.label }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="new-topic">
|
||
<button
|
||
class="pin-btn"
|
||
:title="pinned ? 'Sidebar ausblenden' : 'Sidebar fixieren'"
|
||
@click="emit('togglePin')"
|
||
>{{ pinned ? '⇤' : '⇥' }}</button>
|
||
<button
|
||
class="theme-btn"
|
||
:title="dark ? 'Hellmodus' : 'Dunkelmodus'"
|
||
@click="emit('toggleDark')"
|
||
>{{ dark ? '☀' : '🌙' }}</button>
|
||
<div v-if="selectedTopic" class="ansicht-toggle">
|
||
<button :class="{ active: ansichtModus === 'kompakt' }" title="Kurzer Text" @click="emit('setAnsicht', 'kompakt')">
|
||
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
|
||
<rect x="2" y="5" width="12" height="1.6" rx="0.8" />
|
||
<rect x="2" y="9" width="7" height="1.6" rx="0.8" />
|
||
</svg>
|
||
</button>
|
||
<button :class="{ active: ansichtModus === 'erklärend' }" title="Langer Text" @click="emit('setAnsicht', 'erklärend')">
|
||
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
|
||
<rect x="2" y="3" width="12" height="1.4" rx="0.7" />
|
||
<rect x="2" y="6.3" width="12" height="1.4" rx="0.7" />
|
||
<rect x="2" y="9.6" width="12" height="1.4" rx="0.7" />
|
||
<rect x="2" y="12.9" width="8" height="1.4" rx="0.7" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div v-if="selectedTopic" class="stufe-toggle" title="Tiefe der Guide-Ansicht">
|
||
<button v-for="s in STUFEN_ANSICHT" :key="s.k"
|
||
:class="{ active: stufeAnsicht === s.k }" :title="s.titel"
|
||
@click="emit('setStufe', s.k)">{{ s.label }}</button>
|
||
</div>
|
||
<button class="new-topic-toggle" :class="{ active: dlg }" title="Thema erstellen" @click="toggleErstellen">+</button>
|
||
</div>
|
||
|
||
<!-- 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="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>
|
||
<button :class="{ active: form.sourceType === 'projekt' }" @click="form.sourceType = 'projekt'; form.sourceOrt = ''">Projekt</button>
|
||
<button :class="{ active: form.sourceType === 'uni' }" @click="form.sourceType = 'uni'; form.sourceOrt = ''">Uni</button>
|
||
</div>
|
||
<input
|
||
v-if="form.sourceType === 'link'"
|
||
class="dlg-input" v-model="form.sourceOrt"
|
||
placeholder="https://…" @keyup.enter="createThema"
|
||
/>
|
||
<select
|
||
v-else-if="form.sourceType === 'projekt' || form.sourceType === 'uni'"
|
||
class="dlg-input" v-model="form.sourceOrt"
|
||
>
|
||
<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>
|
||
</div>
|
||
</div>
|
||
<div class="provider-toggle" v-if="providers.length">
|
||
<button
|
||
v-for="p in providers"
|
||
:key="p.id"
|
||
:class="{ active: p.id === provider }"
|
||
:disabled="!p.available"
|
||
:title="p.available ? '' : 'Nicht konfiguriert (CLI/Key fehlt)'"
|
||
@click="emit('setProvider', p.id)"
|
||
>{{ PROVIDER_LABELS[p.id] || p.id }}</button>
|
||
</div>
|
||
<div class="format-section" v-if="selectedTopic">
|
||
<div class="format-error ui-error" v-if="uiError">
|
||
<span class="format-error-text">{{ uiError }}</span>
|
||
<button class="format-error-x" title="Ausblenden" @click="emit('dismissUiError')">×</button>
|
||
</div>
|
||
<div class="progress-info" v-if="activeGenerations.length">
|
||
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
|
||
</div>
|
||
<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-if="bausteine.partial && bausteineState !== 'generating'"
|
||
class="resume-badge"
|
||
title="Abgebrochen — Fortsetzen möglich"
|
||
>Pausiert</span>
|
||
<span class="step-dots">
|
||
<span
|
||
v-for="(s, i) in (bausteine.steps || [])"
|
||
:key="s.label"
|
||
class="step-pill"
|
||
:class="[s.state, { sel: gewaehltePhase === i + 1, klickbar: phasenWaehlbar }]"
|
||
:title="(s.state === 'active' ? (bausteine.progress || s.label) : s.label) + (phasenWaehlbar ? ' — Klick: ab hier neu' : '')"
|
||
@click.stop="phaseKlick(i + 1)"
|
||
>{{ i + 1 }}</span>
|
||
</span>
|
||
</button>
|
||
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')">▾</button>
|
||
</div>
|
||
<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 ? (gewaehltePhase ? `Ab «${gewaehltesLabel}» neu` : 'Neu generieren') : 'Generieren' }}</button>
|
||
<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 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>
|
||
<!-- Formate stehen per CSS-order nach der Bausteine-Zeile (order 2) -->
|
||
<div v-for="f in formats" :key="f.key" :style="{ order: 3 }">
|
||
<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 — Fortsetzen möglich"
|
||
>Pausiert</span>
|
||
<span class="step-dots" v-if="guideSteps(f.key).length">
|
||
<span
|
||
v-for="(s, i) in guideSteps(f.key)"
|
||
:key="s.label"
|
||
class="step-pill"
|
||
:class="[s.state, { sel: gewaehlterStep[f.key] === i + 1, klickbar: guideWaehlbar(f.key) }]"
|
||
:title="(s.state === 'active' ? (latestByFormat[f.key]?.progress || s.label) : s.label) + (guideWaehlbar(f.key) ? ' — Klick: ab hier neu' : '')"
|
||
@click.stop="guideStepKlick(f.key, i + 1)"
|
||
>{{ i + 1 }}</span>
|
||
</span>
|
||
</button>
|
||
<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)"
|
||
>{{ gewaehlterStep[f.key] ? `Ab «${gewaehltesStepLabel(f.key)}» neu` : 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'"
|
||
class="format-progress"
|
||
>{{ latestByFormat[f.key]?.progress || 'Wartend…' }}</div>
|
||
<div v-if="errorMsg(f.key)" class="format-error">
|
||
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
||
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="format-row ord-pruefung">
|
||
<button class="format-name elements-btn" @click="emit('generalExam')">
|
||
<span class="format-label">Allgemeine Prüfung</span>
|
||
</button>
|
||
</div>
|
||
<div class="format-row ord-elemente">
|
||
<button class="format-name elements-btn" @click="emit('openElements')">
|
||
<span class="format-label">Elemente</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<ul class="topic-list">
|
||
<li
|
||
v-for="t in topics"
|
||
:key="t"
|
||
:class="{ active: t === selectedTopic, 'li-open': isOpen('topic-' + t) }"
|
||
>
|
||
<div class="topic-row">
|
||
<span class="topic-name" @click="emit('select', t)">{{ t }}</span>
|
||
<button class="panel-toggle" :class="{ open: isOpen('topic-' + t) }" title="Optionen" @click.stop="toggleTopicPanel(t)">▾</button>
|
||
</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>
|
||
</ul>
|
||
</aside>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.sidebar {
|
||
width: 300px;
|
||
min-width: 300px;
|
||
background: var(--panel);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100dvh;
|
||
}
|
||
|
||
.new-topic {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 0.75rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.new-topic input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 6px 8px;
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 6px;
|
||
font-size: 0.85rem;
|
||
outline: none;
|
||
}
|
||
|
||
.new-topic input:focus {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.new-topic button {
|
||
padding: 6px 10px;
|
||
border: none;
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
border-radius: 6px;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.new-topic button:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.new-topic .pin-btn {
|
||
background: var(--bg);
|
||
color: var(--text-muted);
|
||
border: 1px solid var(--border-strong);
|
||
font-weight: 600;
|
||
padding: 6px 8px;
|
||
}
|
||
|
||
.new-topic .pin-btn:hover {
|
||
background: var(--accent-soft);
|
||
color: var(--accent-hover);
|
||
border-color: var(--accent-border);
|
||
}
|
||
|
||
.new-topic .theme-btn {
|
||
background: var(--bg);
|
||
color: var(--text-muted);
|
||
border: 1px solid var(--border-strong);
|
||
font-weight: 600;
|
||
padding: 6px 8px;
|
||
}
|
||
|
||
.new-topic .theme-btn:hover {
|
||
background: var(--accent-soft);
|
||
color: var(--accent-hover);
|
||
border-color: var(--accent-border);
|
||
}
|
||
|
||
/* Ansicht-Umschalter: zwei Icon-Buttons (kurzer / langer Text) */
|
||
.ansicht-toggle { display: inline-flex; }
|
||
.new-topic .ansicht-toggle button {
|
||
padding: 4px 7px;
|
||
background: var(--bg);
|
||
color: var(--text-muted);
|
||
border: 1px solid var(--border-strong);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
.new-topic .ansicht-toggle button:first-child { border-radius: 6px 0 0 6px; }
|
||
.new-topic .ansicht-toggle button:last-child { border-radius: 0 6px 6px 0; border-left: none; }
|
||
.new-topic .ansicht-toggle button:hover { color: var(--accent-hover); }
|
||
.new-topic .ansicht-toggle button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||
.ansicht-toggle svg { fill: currentColor; display: block; }
|
||
.stufe-toggle { display: inline-flex; margin-left: 4px; }
|
||
.new-topic .stufe-toggle button {
|
||
padding: 4px 6px;
|
||
background: var(--bg);
|
||
color: var(--text-muted);
|
||
border: 1px solid var(--border-strong);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
min-width: 20px;
|
||
}
|
||
.new-topic .stufe-toggle button:first-child { border-radius: 6px 0 0 6px; }
|
||
.new-topic .stufe-toggle button:last-child { border-radius: 0 6px 6px 0; }
|
||
.new-topic .stufe-toggle button:not(:first-child) { border-left: none; }
|
||
.new-topic .stufe-toggle button:hover { color: var(--accent-hover); }
|
||
.new-topic .stufe-toggle button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||
|
||
.stats-bar {
|
||
display: flex;
|
||
padding: 0.5rem 0.75rem;
|
||
gap: 4px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.stat {
|
||
position: relative;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 7px 2px 4px;
|
||
background: var(--panel-soft);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
cursor: default;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 0.8rem;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* Mini-Titel sitzt auf der oberen Kante des Badges (Legenden-Look) */
|
||
.stat-label {
|
||
position: absolute;
|
||
top: -0.42rem;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: 0 3px;
|
||
font-size: 0.5rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
color: var(--text-faint);
|
||
background: var(--panel);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.provider-toggle {
|
||
display: flex;
|
||
gap: 0;
|
||
padding: 0.5rem 0.75rem 0;
|
||
}
|
||
|
||
.provider-toggle button {
|
||
flex: 1;
|
||
padding: 5px 8px;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
border: 1px solid var(--border-strong);
|
||
background: var(--bg);
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.provider-toggle button:first-child {
|
||
border-radius: 6px 0 0 6px;
|
||
}
|
||
|
||
.provider-toggle button:last-child {
|
||
border-radius: 0 6px 6px 0;
|
||
border-left: none;
|
||
}
|
||
|
||
.provider-toggle button.active {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
color: var(--on-accent);
|
||
}
|
||
|
||
.provider-toggle button:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.topic-list {
|
||
list-style: none;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 0.5rem 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.topic-list li {
|
||
font-size: 0.9rem;
|
||
color: var(--text);
|
||
}
|
||
|
||
.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-row {
|
||
background: var(--accent-soft);
|
||
color: var(--accent-hover);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.topic-name {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Format section */
|
||
.bausteine-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.step-dots {
|
||
display: inline-flex;
|
||
gap: 5px;
|
||
flex: 1;
|
||
}
|
||
|
||
/* Grobe Phasen als nummerierte Pillen (1–5) — Anzeige + anklickbar für Re-Run ab hier. */
|
||
.step-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 17px;
|
||
height: 17px;
|
||
border-radius: 50%;
|
||
background: var(--border-strong);
|
||
color: var(--bg);
|
||
font-size: 0.62rem;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
flex-shrink: 0;
|
||
border: 1.5px solid transparent;
|
||
}
|
||
|
||
.step-pill.done {
|
||
background: var(--success-border);
|
||
}
|
||
|
||
.step-pill.active {
|
||
background: var(--warning-border);
|
||
animation: dot-pulse 1.2s ease-in-out infinite;
|
||
}
|
||
|
||
.step-pill.klickbar {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.step-pill.sel {
|
||
border-color: var(--text);
|
||
box-shadow: 0 0 0 1px var(--text);
|
||
}
|
||
|
||
@keyframes dot-pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.35; }
|
||
}
|
||
|
||
.action-btn:disabled {
|
||
opacity: 0.35;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.format-section {
|
||
flex-shrink: 0;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
padding: 0.5rem 0;
|
||
/* flex + order: Bausteine (order 2) vor den Formaten (order 3) */
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.ord-bausteine {
|
||
order: 2;
|
||
}
|
||
|
||
.ord-pruefung {
|
||
order: 4;
|
||
}
|
||
|
||
.ord-elemente {
|
||
order: 5;
|
||
}
|
||
|
||
.elements-btn {
|
||
cursor: pointer;
|
||
color: var(--text);
|
||
}
|
||
|
||
.elements-btn:hover {
|
||
background: var(--panel-soft);
|
||
}
|
||
|
||
.progress-info {
|
||
padding: 0.4rem 0.75rem;
|
||
font-size: 0.75rem;
|
||
color: var(--warning);
|
||
background: var(--warning-soft);
|
||
margin-bottom: 0.25rem;
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
/* Abgewiesene Aktion (409/400) — oberhalb aller Format-Zeilen */
|
||
.ui-error {
|
||
order: 0;
|
||
padding: 0.4rem 0.75rem;
|
||
background: var(--warning-soft);
|
||
}
|
||
|
||
/* Fortschritts-Text direkt unter der laufenden Format-Zeile */
|
||
.format-progress {
|
||
padding: 0 0.75rem 5px calc(0.75rem + 8px);
|
||
font-size: 0.72rem;
|
||
color: var(--warning);
|
||
line-height: 1.3;
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
.resume-badge {
|
||
flex: 0 0 auto;
|
||
font-size: 0.6rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
color: var(--warning);
|
||
background: var(--warning-soft);
|
||
border: 1px solid var(--warning-border);
|
||
border-radius: 4px;
|
||
padding: 1px 5px;
|
||
}
|
||
|
||
.format-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.4rem 0.75rem;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.format-row:hover {
|
||
background: var(--panel-soft);
|
||
}
|
||
|
||
.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: pointer;
|
||
color: var(--text-faint);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
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);
|
||
font-size: 1.1rem;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
padding: 0 2px;
|
||
}
|
||
|
||
.format-name:hover .format-x {
|
||
display: inline;
|
||
}
|
||
|
||
/* Laufend/pausiert: × immer zeigen — Hover gibt es auf Touch nicht */
|
||
.fmt-generating .format-x,
|
||
.fmt-queued .format-x,
|
||
.fmt-paused .format-x,
|
||
.bausteine-row.is-active .format-x {
|
||
display: inline;
|
||
}
|
||
|
||
.format-x.armed,
|
||
.format-error-x.armed,
|
||
.delete-topic.armed {
|
||
display: inline-block;
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
background: var(--danger);
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
padding: 2px 6px;
|
||
}
|
||
|
||
.fmt-done .format-name {
|
||
color: var(--success);
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
background: var(--success-soft);
|
||
border: 1px solid var(--success-border);
|
||
}
|
||
|
||
.fmt-done .format-name:hover {
|
||
background: var(--success-soft-hover);
|
||
}
|
||
|
||
.fmt-generating .format-name,
|
||
.fmt-queued .format-name {
|
||
color: var(--warning);
|
||
background: var(--warning-soft);
|
||
border: 1px solid var(--warning-border);
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
.format-error {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 4px;
|
||
padding: 2px 0.75rem 6px calc(0.75rem + 8px);
|
||
font-size: 0.72rem;
|
||
color: var(--danger);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.format-error-text {
|
||
flex: 1;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.format-error-x {
|
||
flex: 0 0 auto;
|
||
background: none;
|
||
border: none;
|
||
color: var(--danger);
|
||
font-size: 1rem;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
padding: 0 2px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.format-error-x:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.format-actions {
|
||
display: flex;
|
||
gap: 2px;
|
||
margin-left: 6px;
|
||
}
|
||
|
||
.action-btn {
|
||
background: none;
|
||
border: 1px solid transparent;
|
||
border-radius: 4px;
|
||
width: 26px;
|
||
height: 26px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.action-btn.play {
|
||
color: var(--success);
|
||
}
|
||
|
||
.action-btn.play:hover {
|
||
background: var(--success-soft);
|
||
border-color: var(--success-border);
|
||
}
|
||
|
||
.action-btn:disabled {
|
||
opacity: 0.35;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.action-btn:disabled:hover {
|
||
background: none;
|
||
border-color: transparent;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.65; }
|
||
}
|
||
|
||
/* Erstellen — inline aufklappbar (kein Modal) */
|
||
.new-topic-toggle {
|
||
margin-left: auto; /* rechtsbündig statt volle Breite */
|
||
padding: 6px 12px;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 1.1rem;
|
||
line-height: 1;
|
||
text-align: center;
|
||
}
|
||
.new-topic-toggle:hover { background: var(--accent-hover); }
|
||
.new-topic-toggle.active { background: var(--accent-hover); }
|
||
.thema-panel {
|
||
margin: 0.4rem 0;
|
||
padding: 0.5rem;
|
||
background: var(--panel-soft);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.35rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
.dlg-input, .dlg-textarea {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 0.4rem 0.5rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--panel);
|
||
color: var(--text);
|
||
font: inherit;
|
||
}
|
||
.dlg-input:focus, .dlg-textarea:focus { outline: none; border-color: var(--accent); }
|
||
.dlg-textarea { resize: vertical; min-height: 2rem; }
|
||
.dlg-sources { display: flex; gap: 0.3rem; }
|
||
.dlg-sources button {
|
||
flex: 1;
|
||
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.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.4rem; margin-top: 0.1rem; }
|
||
.dlg-actions button {
|
||
padding: 0.4rem 0.8rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--panel-soft);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
}
|
||
.dlg-create { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||
.dlg-create:disabled { opacity: 0.45; cursor: default; }
|
||
|
||
/* Thema-Edit-Panel: dezent in die Liste eingefügt — keine Box, nur ein linker Akzent */
|
||
.edit-panel {
|
||
margin: 0;
|
||
padding: 0.5rem 1rem 0.7rem 1.25rem;
|
||
background: var(--accent-soft);
|
||
border: none;
|
||
border-left: 3px solid var(--accent-border);
|
||
border-radius: 0;
|
||
}
|
||
.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>
|