update
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks } from './api.js'
|
||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
|
||||
import { usePolling } from './composables/usePolling.js'
|
||||
import TopicSidebar from './components/TopicSidebar.vue'
|
||||
import TopicDetail from './components/TopicDetail.vue'
|
||||
import BausteineUebersicht from './components/BausteineUebersicht.vue'
|
||||
import ElementsSidebar from './components/elements/ElementsSidebar.vue'
|
||||
import ElementsOverview from './components/ElementsOverview.vue'
|
||||
|
||||
const guides = ref([])
|
||||
const projects = ref([])
|
||||
const backendTopics = ref([])
|
||||
const selectedTopic = ref(null)
|
||||
const previewGuide = ref(null)
|
||||
@@ -24,6 +24,8 @@ const bausteine = ref({ ...EMPTY_BAUSTEINE })
|
||||
const activeBausteine = ref([])
|
||||
const provider = ref(localStorage.getItem('provider') || 'claude')
|
||||
const providers = ref([])
|
||||
const folders = ref({ projekt: [], uni: [] }) // Ordner für die Quellen-Auswahl
|
||||
const bausteineView = ref(false) // Bausteine-Übersicht im Hauptbereich
|
||||
const stats = ref(null)
|
||||
const fortschritt = ref({})
|
||||
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
||||
@@ -47,6 +49,15 @@ function setProvider(id) {
|
||||
localStorage.setItem('provider', id)
|
||||
}
|
||||
|
||||
async function loadFolders() {
|
||||
try {
|
||||
const [projekt, uni] = await Promise.all([fetchFolders('projekt'), fetchFolders('uni')])
|
||||
folders.value = { projekt, uni }
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Ordner:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
try {
|
||||
providers.value = await fetchProviders()
|
||||
@@ -86,12 +97,7 @@ function onSidebarLeave() {
|
||||
if (!sidebarPinned.value) sidebarSticky.value = false
|
||||
}
|
||||
|
||||
const projectNames = computed(() => projects.value.map((p) => p.name))
|
||||
|
||||
const topics = computed(() => {
|
||||
const isProject = new Set(projectNames.value)
|
||||
return backendTopics.value.filter((t) => !isProject.has(t))
|
||||
})
|
||||
const topics = computed(() => backendTopics.value)
|
||||
|
||||
const doneByFormat = computed(() => {
|
||||
const map = {}
|
||||
@@ -174,15 +180,7 @@ async function loadBausteine() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
projects.value = await fetchProjects()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Projekte:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const FORMAT_ORDER = ['OnePager', 'MiniGuide', 'Guide', 'FullGuide']
|
||||
const FORMAT_ORDER = ['OnePager', 'MiniGuide', 'Guide', 'ProGuide', 'FullGuide']
|
||||
|
||||
function autoPreview() {
|
||||
const map = doneByFormat.value
|
||||
@@ -201,6 +199,7 @@ function selectTopic(topic) {
|
||||
sidebarSticky.value = false
|
||||
elementsOpen.value = false
|
||||
elementsView.value = false
|
||||
bausteineView.value = false
|
||||
elementOpenId.value = null
|
||||
localStorage.setItem('lastTopic', topic)
|
||||
loadBausteine()
|
||||
@@ -258,6 +257,24 @@ async function handleCreateThema({ topic, instructions, sourceType, sourceOrt })
|
||||
startPolling()
|
||||
}
|
||||
|
||||
async function handleUpdateQuelle({ topic, type, ort, spec }) {
|
||||
uiError.value = null
|
||||
try {
|
||||
await apiUpdateQuelle(topic, { type, ort, spec })
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
}
|
||||
await loadBausteine() // Schritt-Anzeige folgt ggf. der neuen Quelle (z.B. Link → „Quelle laden")
|
||||
}
|
||||
|
||||
function handleOpenBausteineView() {
|
||||
if (!selectedTopic.value) return
|
||||
bausteineView.value = true
|
||||
elementsView.value = false
|
||||
previewGuide.value = null
|
||||
}
|
||||
|
||||
async function handleFormatClick({ format, instructions }) {
|
||||
if (!selectedTopic.value) return
|
||||
// Kein Duplikat-Start: läuft für Thema+Format schon eine Generierung, ignorieren
|
||||
@@ -277,23 +294,16 @@ async function handleFormatClick({ format, instructions }) {
|
||||
startPolling()
|
||||
}
|
||||
|
||||
async function handleDeleteProject(name) {
|
||||
await apiDeleteProject(name)
|
||||
if (selectedTopic.value === name) {
|
||||
selectedTopic.value = null
|
||||
previewGuide.value = null
|
||||
}
|
||||
await loadProjects()
|
||||
}
|
||||
|
||||
function handlePreview(guide) {
|
||||
previewGuide.value = guide
|
||||
elementsView.value = false
|
||||
bausteineView.value = false
|
||||
}
|
||||
|
||||
function handleOpenElements() {
|
||||
if (!selectedTopic.value) return
|
||||
elementsView.value = true
|
||||
bausteineView.value = false
|
||||
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
|
||||
}
|
||||
|
||||
@@ -338,10 +348,10 @@ async function handleDeleteTopic(topic) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()])
|
||||
await Promise.all([loadGuides(), loadTopics(), loadProviders(), loadFolders()])
|
||||
const savedTopic = localStorage.getItem('lastTopic')
|
||||
const savedFormat = localStorage.getItem('lastFormat')
|
||||
if (savedTopic && [...topics.value, ...projectNames.value].includes(savedTopic)) {
|
||||
if (savedTopic && topics.value.includes(savedTopic)) {
|
||||
selectTopic(savedTopic)
|
||||
await nextTick()
|
||||
const g = doneByFormat.value[savedFormat]
|
||||
@@ -358,7 +368,6 @@ onMounted(async () => {
|
||||
<div v-if="!sidebarPinned && sidebarSticky" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
||||
<TopicSidebar
|
||||
:topics="topics"
|
||||
:projects="projectNames"
|
||||
:selectedTopic="selectedTopic"
|
||||
:stats="stats"
|
||||
:fortschritt="fortschritt"
|
||||
@@ -374,17 +383,19 @@ onMounted(async () => {
|
||||
:dark="darkMode"
|
||||
:provider="provider"
|
||||
:providers="providers"
|
||||
:folders="folders"
|
||||
@setProvider="setProvider"
|
||||
@toggleDark="toggleDark"
|
||||
@select="selectTopic"
|
||||
@create="createTopic"
|
||||
@createThema="handleCreateThema"
|
||||
@updateQuelle="handleUpdateQuelle"
|
||||
@openBausteineView="handleOpenBausteineView"
|
||||
@formatClick="handleFormatClick"
|
||||
@bausteineClick="handleBausteineClick"
|
||||
@cancelBausteine="handleCancelBausteine"
|
||||
@resetBausteine="handleResetBausteine"
|
||||
@deleteTopic="handleDeleteTopic"
|
||||
@deleteProject="handleDeleteProject"
|
||||
@cancelGuide="handleCancel"
|
||||
@deleteGuide="handleDeleteGuide"
|
||||
@dismissError="handleDismissError"
|
||||
@@ -394,8 +405,13 @@ onMounted(async () => {
|
||||
@togglePin="toggleSidebarPin"
|
||||
@sidebarLeave="onSidebarLeave"
|
||||
/>
|
||||
<BausteineUebersicht
|
||||
v-if="selectedTopic && bausteineView"
|
||||
:topic="selectedTopic"
|
||||
@close="bausteineView = false"
|
||||
/>
|
||||
<ElementsOverview
|
||||
v-if="selectedTopic && elementsView"
|
||||
v-else-if="selectedTopic && elementsView"
|
||||
:topic="selectedTopic"
|
||||
:version="elementsVersion"
|
||||
@open="handleOpenElementDetail"
|
||||
|
||||
@@ -124,6 +124,30 @@ export async function fetchProjects() {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchFolders(kind) {
|
||||
const res = await fetch(`${BASE}/folders?kind=${encodeURIComponent(kind)}`)
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function fetchQuelle(topic) {
|
||||
const res = await fetch(`${BASE}/bausteine/quelle?topic=${encodeURIComponent(topic)}`)
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function updateQuelle(topic, { type, ort = '', spec = '' }) {
|
||||
const res = await fetch(`${BASE}/bausteine/quelle`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ topic, type, ort, spec }),
|
||||
})
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function fetchBausteineUebersicht(topic) {
|
||||
const res = await fetch(`${BASE}/bausteine/uebersicht?topic=${encodeURIComponent(topic)}`)
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function deleteProject(name) {
|
||||
await fetch(`${BASE}/projects/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
211
frontend/src/components/BausteineUebersicht.vue
Normal file
211
frontend/src/components/BausteineUebersicht.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user