update
This commit is contained in:
@@ -14,6 +14,7 @@ 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 darkMode = ref(
|
||||
localStorage.getItem('darkMode') === null
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
@@ -374,9 +375,9 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky }">
|
||||
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky, 'sidebar-over-fokus': fokusOffen && sidebarSticky }">
|
||||
<div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
|
||||
<div v-if="!sidebarPinned && sidebarSticky" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
||||
<div v-if="(!sidebarPinned && sidebarSticky) || (fokusOffen && sidebarSticky)" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
||||
<TopicSidebar
|
||||
:topics="topics"
|
||||
:selectedTopic="selectedTopic"
|
||||
@@ -441,6 +442,8 @@ onMounted(async () => {
|
||||
:ansichtModus="ansichtModus"
|
||||
@progressChanged="loadStats(); loadBausteine()"
|
||||
@setAnsicht="setAnsicht"
|
||||
@open-sidebar="sidebarSticky = true"
|
||||
@fokus-active="fokusOffen = $event"
|
||||
/>
|
||||
<div v-else class="empty-main">
|
||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||
@@ -583,6 +586,21 @@ textarea::placeholder {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Baustein-Vollbild: Sidebar als Overlay ÜBER das Fokus-Overlay (z-index 40) heben.
|
||||
Greift für gepinnt UND floating. */
|
||||
.layout.sidebar-over-fokus > .sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100dvh;
|
||||
transform: translateX(0);
|
||||
z-index: 50;
|
||||
box-shadow: 0 0 16px var(--shadow);
|
||||
}
|
||||
.layout.sidebar-over-fokus .sidebar-backdrop {
|
||||
z-index: 49;
|
||||
}
|
||||
|
||||
.empty-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -114,11 +114,6 @@ export async function fetchProviders() {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchProjects() {
|
||||
const res = await fetch(`${BASE}/projects`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchFolders(kind) {
|
||||
const res = await fetch(`${BASE}/folders?kind=${encodeURIComponent(kind)}`)
|
||||
return jsonOrThrow(res)
|
||||
@@ -143,10 +138,6 @@ export async function fetchBausteineUebersicht(topic) {
|
||||
return jsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function deleteProject(name) {
|
||||
await fetch(`${BASE}/projects/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export async function cancelGuide(id) {
|
||||
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const props = defineProps({
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht'])
|
||||
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht', 'openSidebar'])
|
||||
|
||||
const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
|
||||
const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
|
||||
@@ -72,6 +72,7 @@ const standFarbe = computed(() => {
|
||||
<div class="fokus-overlay" :style="{ '--stand': standFarbe }">
|
||||
<div class="fokus-bar">
|
||||
<div class="fokus-bar-inner">
|
||||
<button class="fokus-btn" title="Navigation öffnen" @click="$emit('openSidebar')">☰</button>
|
||||
<button class="fokus-btn" :disabled="!hasPrev" title="Voriger Baustein" @click="$emit('prev')">←</button>
|
||||
<button class="fokus-btn" :disabled="!hasNext" title="Nächster Baustein" @click="$emit('next')">→</button>
|
||||
<span class="fokus-titel">{{ baustein.title }}</span>
|
||||
|
||||
@@ -28,8 +28,8 @@ const VERSTANDEN = 12
|
||||
const GEMEISTERT = 18
|
||||
const VOLL = 30
|
||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false })
|
||||
// Aktiver Modus aus Score-Band (unter 30 fix) + Schwierigkeit (ALT+E). Ab 30 frei.
|
||||
// Ohne Frage-Muster (alter Baustein) gibt es nur Erklären — Quiz/Lückentext brauchen die Saat.
|
||||
// Volle Prüfung (cap 30): durchgehend zufällig aus allen 5 Formen. Kurze Prüfung (cap 6):
|
||||
// Score-Band + Schwierigkeit (ALT+E). Ohne Frage-Muster (alter Baustein) nur Erklären.
|
||||
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
||||
const score = computed(() => st.value.gute_antworten || 0)
|
||||
const schwerWahl = ref(false) // leicht/schwer in den Bändern unter 19 (ALT+E)
|
||||
@@ -42,9 +42,13 @@ function bandFuer(s) {
|
||||
}
|
||||
// Am Format-Max: Score eingefroren („Max"). Gilt auch bei cap 30 (FullGuide, endgültig).
|
||||
const amDeckel = computed(() => score.value >= props.cap)
|
||||
// Aktuelles Band: am Deckel bleibt das Band des erreichten Formats (cap−1), sonst score-basiert.
|
||||
// Aktuelles Band: volle Prüfung (cap 30) ist DURCHGEHEND zufällig aus allen 5 Formen.
|
||||
// Sonst score-basiert (am Deckel das Band des erreichten Formats, cap−1).
|
||||
// Ohne Frage-Muster (alter Baustein) gibt es nur Erklären.
|
||||
const aktuellesBand = computed(() => musterModus.value ? bandFuer(amDeckel.value ? props.cap - 1 : score.value) : 'erklaeren')
|
||||
const aktuellesBand = computed(() =>
|
||||
!musterModus.value ? 'erklaeren'
|
||||
: props.cap >= VOLL ? 'zufall'
|
||||
: bandFuer(amDeckel.value ? props.cap - 1 : score.value))
|
||||
// 'zufall' = Platzhalter; das gezogene Objekt trägt die echte Form/Schwierigkeit.
|
||||
const aktiverModus = computed(() => {
|
||||
const b = aktuellesBand.value
|
||||
@@ -74,10 +78,10 @@ const aktSchwer = computed(() => {
|
||||
return schwer.value
|
||||
})
|
||||
const modusRegel = computed(() => {
|
||||
const m = imZufall.value ? '−3' : '−1' // Zufallband: Fehler kostet −3
|
||||
const m = score.value >= GEMEISTERT ? '−3' : '−1' // Fehler kostet erst ab Score 18 −3
|
||||
if (zeigeForm.value === 'quiz') return aktSchwer.value ? `x von 4 · +3/${m}` : `1 von 4 · +1/${m}`
|
||||
if (zeigeForm.value === 'lueckentext') return aktSchwer.value ? `frei tippen · +3/${m}` : `Begriff aus 4 · +1/${m}`
|
||||
return imZufall.value ? '+1/+2/+3 · Fehler −3' : '+1/+2/+3 · Nachfragen max +1 · schwach −1'
|
||||
return score.value >= GEMEISTERT ? '+1/+2/+3 · Fehler −3' : '+1/+2/+3 · Nachfragen max +1 · schwach −1'
|
||||
})
|
||||
// Ende des aktuellen Bands (nächster Meilenstein).
|
||||
const bandZiel = computed(() => {
|
||||
@@ -689,7 +693,9 @@ function onPruefKey(e) {
|
||||
<div v-else>
|
||||
<p class="bp-hint">
|
||||
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — <strong>Max</strong>.</template>
|
||||
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}<span v-if="imZufall" class="bp-hint-key"> · zufällig</span></template>
|
||||
<template v-else-if="imZufall && !anzeigeForm">Zufällig · {{ score }}/{{ cap }}</template>
|
||||
<template v-else-if="imZufall">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} · {{ modusRegel }}</template>
|
||||
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}</template>
|
||||
</p>
|
||||
<button
|
||||
v-if="!amDeckel && !imZufall && zeigeForm !== 'erklaeren'"
|
||||
|
||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
||||
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||||
})
|
||||
|
||||
const emit = defineEmits(['progressChanged', 'setAnsicht'])
|
||||
const emit = defineEmits(['progressChanged', 'setAnsicht', 'openSidebar', 'fokusActive'])
|
||||
|
||||
// Rotierende Kapitel-Akzentfarben (ohne Rot)
|
||||
const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366f1']
|
||||
@@ -97,6 +97,10 @@ const bausteine = computed(() =>
|
||||
)
|
||||
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
|
||||
|
||||
// Fokus-Zustand an App melden (für die Sidebar über dem Overlay); Themenwechsel schließt den Fokus.
|
||||
watch(fokusIndex, (v) => emit('fokusActive', v !== null))
|
||||
watch(() => props.previewGuide?.id, () => { fokusIndex.value = null })
|
||||
|
||||
function openFokus(baustein, tab) {
|
||||
if (!istPruefbar(baustein)) return // reine Lese-Sections (Rest/Rand) öffnen keinen Prüfungs-Fokus
|
||||
const i = bausteine.value.findIndex((s) => s.title === baustein.title)
|
||||
@@ -286,6 +290,7 @@ function extractContext() {
|
||||
@close="fokusIndex = null"
|
||||
@set-ansicht="$emit('setAnsicht', $event)"
|
||||
@status-changed="(st) => onBausteinStatus(st.baustein, st)"
|
||||
@open-sidebar="$emit('openSidebar')"
|
||||
/>
|
||||
|
||||
<button v-if="previewGuide && !chatOpen && fokusIndex === null" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
|
||||
|
||||
Reference in New Issue
Block a user