update
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, resetBausteineAbStep as apiResetBausteineAbStep, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
|
||||
import { fetchGuides, fetchTopics, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBlocksStatus, fetchActiveBlocks, createBlocks as apiCreateBausteine, resetBlocksFromStep as apiResetBausteineAbStep, cancelBlocks as apiCancelBausteine, deleteBlocks as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicProgress, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateSource 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 BlocksOverview from './components/BlocksOverview.vue'
|
||||
import ElementsSidebar from './components/elements/ElementsSidebar.vue'
|
||||
import ElementsOverview from './components/ElementsOverview.vue'
|
||||
import GeneralExamPanel from './components/GeneralExamPanel.vue'
|
||||
@@ -15,39 +15,38 @@ const selectedTopic = ref(null)
|
||||
const previewGuide = ref(null)
|
||||
const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false')
|
||||
const sidebarSticky = ref(false)
|
||||
const fokusOffen = ref(false) // Baustein-Vollbild aktiv → Sidebar als Overlay über das Fokus-Overlay heben
|
||||
const focusOpen = ref(false) // block fullscreen active → lift sidebar as overlay above the focus overlay
|
||||
const darkMode = ref(
|
||||
localStorage.getItem('darkMode') === null
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: localStorage.getItem('darkMode') === 'true',
|
||||
)
|
||||
const EMPTY_BAUSTEINE = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [], feine_steps: [] }
|
||||
const bausteine = ref({ ...EMPTY_BAUSTEINE })
|
||||
const activeBausteine = ref([])
|
||||
const EMPTY_BLOCKS = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [], feine_steps: [] }
|
||||
const blocks = ref({ ...EMPTY_BLOCKS })
|
||||
const activeBlocks = 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 allgemeinView = ref(false) // Allgemeine Prüfung (themenweit) im Hauptbereich
|
||||
const ansichtModus = ref('kompakt') // kompakt | erklärend — je Thema, Default kompakt
|
||||
const stufeAnsicht = ref(Number(localStorage.getItem('stufe')) || 4) // 1=A · 2=F · 3=E · 4=V (Stufen-Ansicht)
|
||||
const folders = ref({ projekt: [], uni: [] }) // folders for the sources picker
|
||||
const mainView = ref('blocks') // blocks | elements | general | detail — exclusive main-area view
|
||||
const viewMode = ref('compact') // compact | erklärend — per topic, default compact
|
||||
const levelView = ref(Number(localStorage.getItem('level')) || 4) // 1=A · 2=F · 3=E · 4=V (levels view)
|
||||
const stats = ref(null)
|
||||
const fortschritt = ref({})
|
||||
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
||||
const guideStepsDone = ref({}) // höchster fertiger Schritt-Index je Format (artefakt-basiert)
|
||||
const uiError = ref(null) // abgewiesene Aktionen (409/400) sichtbar machen
|
||||
const elementsOpen = ref(false) // rechte Sidebar
|
||||
const elementsView = ref(false) // Übersicht im Hauptbereich
|
||||
const elementsVersion = ref(0) // Erhöhung = Übersicht neu laden
|
||||
const elementOpenId = ref(null) // Element aus Übersicht in Sidebar öffnen
|
||||
const progress = ref({})
|
||||
const locks = ref({}) // lock reasons per format (backend = single rule source)
|
||||
const guideStepsDone = ref({}) // highest finished step index per format (artifact-based)
|
||||
const uiError = ref(null) // surface rejected actions (409/400)
|
||||
const elementsOpen = ref(false) // right sidebar
|
||||
const elementsVersion = ref(0) // increment = reload overview
|
||||
const elementOpenId = ref(null) // open element from overview in sidebar
|
||||
const elementOpenTick = ref(0)
|
||||
|
||||
// Run a loader, log + swallow its error (a failed background load must not break the UI).
|
||||
async function guard(label, fn) {
|
||||
try { return await fn() } catch (e) { console.error(label, e) }
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
stats.value = await fetchStats()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Statistik:', e)
|
||||
}
|
||||
await guard('Failed to load stats:', async () => { stats.value = await fetchStats() })
|
||||
}
|
||||
|
||||
function setProvider(id) {
|
||||
@@ -56,25 +55,21 @@ function setProvider(id) {
|
||||
}
|
||||
|
||||
async function loadFolders() {
|
||||
try {
|
||||
await guard('Failed to load folders:', async () => {
|
||||
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 {
|
||||
await guard('Failed to load providers:', async () => {
|
||||
providers.value = await fetchProviders()
|
||||
const current = providers.value.find((p) => p.id === provider.value)
|
||||
if (current && !current.available) {
|
||||
const fallback = providers.value.find((p) => p.available)
|
||||
if (fallback) setProvider(fallback.id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Provider:', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
@@ -136,12 +131,12 @@ async function loadTopics() {
|
||||
try {
|
||||
backendTopics.value = await fetchTopics()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Themen:', e)
|
||||
console.error('Failed to load topics:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Weggeklickte Fehler bleiben weggeklickt — auch über Reloads (localStorage).
|
||||
// Nicht weggeklickte Fehler bleiben sichtbar, bis der Nutzer sie schließt.
|
||||
// Dismissed errors stay dismissed — even across reloads (localStorage).
|
||||
// Errors that aren't dismissed stay visible until the user closes them.
|
||||
const dismissedErrors = ref(new Set(JSON.parse(localStorage.getItem('dismissedErrors') || '[]')))
|
||||
|
||||
function persistDismissed() {
|
||||
@@ -156,7 +151,7 @@ function handleDismissError(guideId) {
|
||||
async function loadGuides() {
|
||||
try {
|
||||
guides.value = await fetchGuides()
|
||||
// IDs prunen, deren Guide nicht mehr als Fehler existiert
|
||||
// Prune IDs whose guide no longer exists as an error
|
||||
const errorIds = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
|
||||
if ([...dismissedErrors.value].some((id) => !errorIds.has(id))) {
|
||||
dismissedErrors.value = new Set([...dismissedErrors.value].filter((id) => errorIds.has(id)))
|
||||
@@ -164,27 +159,27 @@ async function loadGuides() {
|
||||
}
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden:', e)
|
||||
console.error('Failed to load:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBausteine() {
|
||||
async function loadBlocks() {
|
||||
try {
|
||||
activeBausteine.value = await fetchActiveBausteine()
|
||||
activeBlocks.value = await fetchActiveBlocks()
|
||||
if (selectedTopic.value) {
|
||||
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
||||
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
||||
blocks.value = await fetchBlocksStatus(selectedTopic.value)
|
||||
progress.value = await fetchTopicProgress(selectedTopic.value)
|
||||
locks.value = await fetchGuideLocks(selectedTopic.value)
|
||||
guideStepsDone.value = await fetchGuideSteps(selectedTopic.value)
|
||||
} else {
|
||||
bausteine.value = { ...EMPTY_BAUSTEINE }
|
||||
fortschritt.value = {}
|
||||
blocks.value = { ...EMPTY_BLOCKS }
|
||||
progress.value = {}
|
||||
locks.value = {}
|
||||
guideStepsDone.value = {}
|
||||
}
|
||||
if (activeBausteine.value.length && !polling.running()) startPolling()
|
||||
if (activeBlocks.value.length && !polling.running()) startPolling()
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Bausteine:', e)
|
||||
console.error('Failed to load blocks:', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,67 +188,57 @@ function selectTopic(topic) {
|
||||
previewGuide.value = null
|
||||
sidebarSticky.value = false
|
||||
elementsOpen.value = false
|
||||
elementsView.value = false
|
||||
bausteineView.value = true // Thema-Klick → Baustein-Übersicht (Guide erst auf Pill-Klick)
|
||||
allgemeinView.value = false
|
||||
mainView.value = 'blocks' // topic click → blocks overview (guide only on pill click)
|
||||
elementOpenId.value = null
|
||||
ansichtModus.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'kompakt'
|
||||
viewMode.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'compact'
|
||||
localStorage.setItem('lastTopic', topic)
|
||||
loadBausteine()
|
||||
loadBlocks()
|
||||
}
|
||||
|
||||
// Beim Reload dort landen, wo man vorher war (Thema + Format)
|
||||
// On reload, land where you were before (topic + format)
|
||||
watch(previewGuide, (g) => {
|
||||
if (g) localStorage.setItem('lastFormat', g.format)
|
||||
})
|
||||
|
||||
async function createTopic(topic) {
|
||||
await apiCreateTopic(topic)
|
||||
await loadTopics()
|
||||
selectedTopic.value = topic
|
||||
previewGuide.value = null
|
||||
loadBausteine()
|
||||
}
|
||||
|
||||
async function handleCancelBausteine() {
|
||||
async function handleCancelBlocks() {
|
||||
if (!selectedTopic.value) return
|
||||
await apiCancelBausteine(selectedTopic.value)
|
||||
await loadBausteine()
|
||||
await loadBlocks()
|
||||
}
|
||||
|
||||
async function handleResetBausteine() {
|
||||
async function handleResetBlocks() {
|
||||
if (!selectedTopic.value) return
|
||||
await apiDeleteBausteine(selectedTopic.value)
|
||||
await loadBausteine()
|
||||
await loadBlocks()
|
||||
}
|
||||
|
||||
async function handleResetAbStep(step) {
|
||||
async function handleResetFromStep(step) {
|
||||
if (!selectedTopic.value) return
|
||||
uiError.value = null
|
||||
try {
|
||||
await apiResetBausteineAbStep(selectedTopic.value, step) // nur zurücksetzen, kein Neu-Generieren
|
||||
await apiResetBausteineAbStep(selectedTopic.value, step) // only reset, no regeneration
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
}
|
||||
await loadBausteine()
|
||||
await loadBlocks()
|
||||
}
|
||||
|
||||
async function handleBausteineClick({ instructions, abPhase = null, abStep = null }) {
|
||||
async function handleBlocksClick({ instructions, abPhase = null, abStep = null }) {
|
||||
if (!selectedTopic.value) return
|
||||
uiError.value = null
|
||||
try {
|
||||
// Quelle ist hier schon festgeschrieben; abPhase/abStep steuern den Re-Run-Umfang.
|
||||
// Source is already fixed here; abPhase/abStep control the re-run scope.
|
||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase, abStep)
|
||||
} catch (e) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
}
|
||||
await loadBausteine()
|
||||
await loadBlocks()
|
||||
startPolling()
|
||||
}
|
||||
|
||||
async function handleCreateThema({ topic, instructions, sourceType, sourceOrt }) {
|
||||
async function handleCreateTopic({ topic, instructions, sourceType, sourceOrt }) {
|
||||
uiError.value = null
|
||||
try {
|
||||
await apiCreateBausteine(topic, instructions, provider.value, sourceType, sourceOrt)
|
||||
@@ -266,7 +251,7 @@ async function handleCreateThema({ topic, instructions, sourceType, sourceOrt })
|
||||
startPolling()
|
||||
}
|
||||
|
||||
async function handleUpdateQuelle({ topic, type, ort, spec }) {
|
||||
async function handleUpdateSource({ topic, type, ort, spec }) {
|
||||
uiError.value = null
|
||||
try {
|
||||
await apiUpdateQuelle(topic, { type, ort, spec })
|
||||
@@ -274,30 +259,28 @@ async function handleUpdateQuelle({ topic, type, ort, spec }) {
|
||||
uiError.value = e.message
|
||||
return
|
||||
}
|
||||
await loadBausteine() // Schritt-Anzeige folgt ggf. der neuen Quelle (z.B. Link → „Quelle laden")
|
||||
await loadBlocks() // the step display may follow the new source (e.g. link → "load source")
|
||||
}
|
||||
|
||||
function setAnsicht(modus) {
|
||||
ansichtModus.value = modus
|
||||
function setView(modus) {
|
||||
viewMode.value = modus
|
||||
if (selectedTopic.value) localStorage.setItem('ansicht_' + selectedTopic.value, modus)
|
||||
}
|
||||
|
||||
function setStufe(k) {
|
||||
stufeAnsicht.value = k
|
||||
localStorage.setItem('stufe', String(k))
|
||||
function setLevel(k) {
|
||||
levelView.value = k
|
||||
localStorage.setItem('level', String(k))
|
||||
}
|
||||
|
||||
function handleOpenBausteineView() {
|
||||
function handleOpenBlocksView() {
|
||||
if (!selectedTopic.value) return
|
||||
bausteineView.value = true
|
||||
elementsView.value = false
|
||||
allgemeinView.value = false
|
||||
mainView.value = 'blocks'
|
||||
previewGuide.value = null
|
||||
}
|
||||
|
||||
async function handleFormatClick({ format, instructions, abStep = null }) {
|
||||
if (!selectedTopic.value) return
|
||||
// Kein Duplikat-Start: läuft für Thema+Format schon eine Generierung, ignorieren
|
||||
// No duplicate start: if a generation is already running for topic+format, ignore
|
||||
const running = guides.value.some(
|
||||
(g) => g.topic === selectedTopic.value && g.format === format
|
||||
&& (g.status === 'generating' || g.status === 'queued'),
|
||||
@@ -316,25 +299,19 @@ async function handleFormatClick({ format, instructions, abStep = null }) {
|
||||
|
||||
function handlePreview(guide) {
|
||||
previewGuide.value = guide
|
||||
elementsView.value = false
|
||||
bausteineView.value = false
|
||||
allgemeinView.value = false
|
||||
mainView.value = 'detail'
|
||||
}
|
||||
|
||||
function handleGeneralExam() {
|
||||
if (!selectedTopic.value) return
|
||||
allgemeinView.value = true
|
||||
bausteineView.value = false
|
||||
elementsView.value = false
|
||||
mainView.value = 'general'
|
||||
previewGuide.value = null
|
||||
}
|
||||
|
||||
function handleOpenElements() {
|
||||
if (!selectedTopic.value) return
|
||||
elementsView.value = true
|
||||
bausteineView.value = false
|
||||
allgemeinView.value = false
|
||||
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
|
||||
mainView.value = 'elements'
|
||||
// Right sidebar stays closed — it opens only when an element is clicked.
|
||||
}
|
||||
|
||||
function handleOpenElementDetail(el) {
|
||||
@@ -352,8 +329,8 @@ async function handleDeleteGuide(guideId, slots = false) {
|
||||
}
|
||||
|
||||
const polling = usePolling(
|
||||
() => Promise.all([loadGuides(), loadBausteine(), loadTopics()]),
|
||||
() => hasActiveGuides.value || activeBausteine.value.length > 0,
|
||||
() => Promise.all([loadGuides(), loadBlocks(), loadTopics()]),
|
||||
() => hasActiveGuides.value || activeBlocks.value.length > 0,
|
||||
)
|
||||
const startPolling = polling.start
|
||||
|
||||
@@ -381,7 +358,7 @@ onMounted(async () => {
|
||||
await Promise.all([loadGuides(), loadTopics(), loadProviders(), loadFolders()])
|
||||
const savedTopic = localStorage.getItem('lastTopic')
|
||||
if (savedTopic && topics.value.includes(savedTopic)) {
|
||||
selectTopic(savedTopic) // landet auf der Baustein-Übersicht
|
||||
selectTopic(savedTopic) // lands on the blocks overview
|
||||
} else if (!selectedTopic.value && topics.value.length) {
|
||||
selectTopic(topics.value[0])
|
||||
}
|
||||
@@ -389,14 +366,14 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky, 'sidebar-over-fokus': fokusOffen && sidebarSticky }">
|
||||
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky, 'sidebar-over-fokus': focusOpen && sidebarSticky }">
|
||||
<div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
|
||||
<div v-if="(!sidebarPinned && sidebarSticky) || (fokusOffen && sidebarSticky)" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
||||
<div v-if="(!sidebarPinned && sidebarSticky) || (focusOpen && sidebarSticky)" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
||||
<TopicSidebar
|
||||
:topics="topics"
|
||||
:selectedTopic="selectedTopic"
|
||||
:stats="stats"
|
||||
:fortschritt="fortschritt"
|
||||
:fortschritt="progress"
|
||||
:locks="locks"
|
||||
:guideStepsDone="guideStepsDone"
|
||||
:uiError="uiError"
|
||||
@@ -404,29 +381,28 @@ onMounted(async () => {
|
||||
:latestByFormat="latestByFormat"
|
||||
:allGuides="guides"
|
||||
:dismissedErrors="dismissedErrors"
|
||||
:bausteine="bausteine"
|
||||
:activeBausteine="activeBausteine"
|
||||
:blocks="blocks"
|
||||
:activeBausteine="activeBlocks"
|
||||
:pinned="sidebarPinned"
|
||||
:dark="darkMode"
|
||||
:provider="provider"
|
||||
:providers="providers"
|
||||
:folders="folders"
|
||||
:ansichtModus="ansichtModus"
|
||||
:stufeAnsicht="stufeAnsicht"
|
||||
:ansichtModus="viewMode"
|
||||
:stufeAnsicht="levelView"
|
||||
@setProvider="setProvider"
|
||||
@toggleDark="toggleDark"
|
||||
@setAnsicht="setAnsicht"
|
||||
@setStufe="setStufe"
|
||||
@setAnsicht="setView"
|
||||
@setStufe="setLevel"
|
||||
@generalExam="handleGeneralExam"
|
||||
@select="selectTopic"
|
||||
@create="createTopic"
|
||||
@createThema="handleCreateThema"
|
||||
@updateQuelle="handleUpdateQuelle"
|
||||
@openBausteineView="handleOpenBausteineView"
|
||||
@createThema="handleCreateTopic"
|
||||
@updateSource="handleUpdateSource"
|
||||
@openBausteineView="handleOpenBlocksView"
|
||||
@formatClick="handleFormatClick"
|
||||
@bausteineClick="handleBausteineClick"
|
||||
@cancelBausteine="handleCancelBausteine"
|
||||
@resetBausteine="handleResetBausteine"
|
||||
@bausteineClick="handleBlocksClick"
|
||||
@cancelBlocks="handleCancelBlocks"
|
||||
@resetBausteine="handleResetBlocks"
|
||||
@deleteTopic="handleDeleteTopic"
|
||||
@cancelGuide="handleCancel"
|
||||
@deleteGuide="handleDeleteGuide"
|
||||
@@ -437,33 +413,33 @@ onMounted(async () => {
|
||||
@togglePin="toggleSidebarPin"
|
||||
@sidebarLeave="onSidebarLeave"
|
||||
/>
|
||||
<BausteineUebersicht
|
||||
v-if="selectedTopic && bausteineView"
|
||||
<BlocksOverview
|
||||
v-if="selectedTopic && mainView === 'blocks'"
|
||||
:topic="selectedTopic"
|
||||
:steps="bausteine.feine_steps || []"
|
||||
:generating="bausteine.generating"
|
||||
:progress="bausteine.progress"
|
||||
:ready="bausteine.ready"
|
||||
:partial="bausteine.partial"
|
||||
@close="bausteineView = false"
|
||||
@restartFrom="(i) => handleBausteineClick({ instructions: '', abStep: i })"
|
||||
@resetFrom="handleResetAbStep"
|
||||
@restartAll="() => handleBausteineClick({ abPhase: bausteine.ready ? 1 : null })"
|
||||
@removeAll="handleResetBausteine"
|
||||
@cancel="handleCancelBausteine"
|
||||
:steps="blocks.feine_steps || []"
|
||||
:generating="blocks.generating"
|
||||
:progress="blocks.progress"
|
||||
:ready="blocks.ready"
|
||||
:partial="blocks.partial"
|
||||
@close="mainView = 'detail'"
|
||||
@restartFrom="(i) => handleBlocksClick({ instructions: '', abStep: i })"
|
||||
@resetFrom="handleResetFromStep"
|
||||
@restartAll="() => handleBlocksClick({ abPhase: blocks.ready ? 1 : null })"
|
||||
@removeAll="handleResetBlocks"
|
||||
@cancel="handleCancelBlocks"
|
||||
/>
|
||||
<ElementsOverview
|
||||
v-else-if="selectedTopic && elementsView"
|
||||
v-else-if="selectedTopic && mainView === 'elements'"
|
||||
:topic="selectedTopic"
|
||||
:version="elementsVersion"
|
||||
@open="handleOpenElementDetail"
|
||||
/>
|
||||
<GeneralExamPanel
|
||||
v-else-if="selectedTopic && allgemeinView"
|
||||
v-else-if="selectedTopic && mainView === 'general'"
|
||||
:topic="selectedTopic"
|
||||
:provider="provider"
|
||||
@progressChanged="loadStats(); loadBausteine()"
|
||||
@fokus-active="fokusOffen = $event"
|
||||
@progressChanged="loadStats(); loadBlocks()"
|
||||
@fokus-active="focusOpen = $event"
|
||||
/>
|
||||
<TopicDetail
|
||||
v-else-if="selectedTopic"
|
||||
@@ -472,16 +448,16 @@ onMounted(async () => {
|
||||
:provider="provider"
|
||||
:elementsOpen="elementsOpen"
|
||||
:doneByFormat="doneByFormat"
|
||||
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
|
||||
:ansichtModus="ansichtModus"
|
||||
:stufeAnsicht="stufeAnsicht"
|
||||
@progressChanged="loadStats(); loadBausteine()"
|
||||
@setAnsicht="setAnsicht"
|
||||
:themaAbgeschlossen="!!progress.completed"
|
||||
:ansichtModus="viewMode"
|
||||
:stufeAnsicht="levelView"
|
||||
@progressChanged="loadStats(); loadBlocks()"
|
||||
@setAnsicht="setView"
|
||||
@open-sidebar="sidebarSticky = true"
|
||||
@fokus-active="fokusOffen = $event"
|
||||
@fokus-active="focusOpen = $event"
|
||||
/>
|
||||
<div v-else class="empty-main">
|
||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||
<p>Create or select a topic in the sidebar.</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="elementsOpen && selectedTopic"
|
||||
@@ -520,11 +496,11 @@ onMounted(async () => {
|
||||
--success-soft: #d1fae5;
|
||||
--success-soft-hover: #a7f3d0;
|
||||
--success-border: #34d399;
|
||||
/* 4 Lernstufen: grün/blau/lila/gold = Anfänger/Fortgeschritten/Experte/Meister */
|
||||
--stufe-anfaenger: #22c55e;
|
||||
--stufe-fortgeschritten: #3b82f6;
|
||||
--stufe-experte: #8b5cf6;
|
||||
--stufe-meister: #d4af37;
|
||||
/* 4 learning levels: green/blue/purple/gold = Beginner/Advanced/Expert/Master */
|
||||
--level-beginner: #22c55e;
|
||||
--level-advanced: #3b82f6;
|
||||
--level-expert: #8b5cf6;
|
||||
--level-master: #d4af37;
|
||||
--warning: #92400e;
|
||||
--warning-soft: #fef3c7;
|
||||
--warning-border: #fbbf24;
|
||||
@@ -600,8 +576,8 @@ textarea::placeholder {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Unsichtbare Fläche hinter der offenen Floating-Sidebar.
|
||||
Tipp/Klick daneben schließt sie — ohne sie gibt es auf Touch keinen Ausweg. */
|
||||
/* Invisible surface behind the open floating sidebar.
|
||||
A tap/click next to it closes it — without it there's no way out on touch. */
|
||||
.sidebar-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -626,8 +602,8 @@ textarea::placeholder {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Baustein-Vollbild: Sidebar als Overlay ÜBER das Fokus-Overlay (z-index 40) heben.
|
||||
Greift für gepinnt UND floating. */
|
||||
/* Block fullscreen: lift the sidebar as an overlay ABOVE the focus overlay (z-index 40).
|
||||
Applies for pinned AND floating. */
|
||||
.layout.sidebar-over-fokus > .sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -650,8 +626,8 @@ textarea::placeholder {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Nur sichtbar, wenn die Elemente-Sidebar mobil als Overlay liegt.
|
||||
Tipp daneben schließt sie. */
|
||||
/* Only visible when the elements sidebar sits as an overlay on mobile.
|
||||
A tap next to it closes it. */
|
||||
.elements-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user