This commit is contained in:
team3
2026-06-17 16:41:04 +02:00
parent 76a405bab9
commit 61a22a4d9f
3 changed files with 249 additions and 74 deletions

View File

@@ -4,6 +4,7 @@ import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import { useChat } from '../composables/useChat.js'
import BausteinPanel from './BausteinPanel.vue'
import BausteinFokus from './BausteinFokus.vue'
const props = defineProps({
previewGuide: { type: Object, default: null },
@@ -57,6 +58,35 @@ function onBausteinStatus(baustein, status) {
if (status.absolviert && !warAbsolviert) emit('progressChanged') // Locks/Stats neu laden
}
// --- Vollbild-Fokus: ein Baustein groß, Guide links + Prüfung rechts ---
const fokusIndex = ref(null) // Index in der flachen Baustein-Liste; null = zu
const fokusTab = ref('pruefung')
const bausteine = computed(() =>
isOnePager.value || !content.value ? [] : content.value.chapters.flatMap((ch) => ch.sections),
)
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
function openFokus(baustein, tab) {
const i = bausteine.value.findIndex((s) => s.title === baustein.title)
if (i === -1) return
fokusIndex.value = i
fokusTab.value = tab || 'pruefung'
}
function fokusPrev() { if (fokusIndex.value > 0) fokusIndex.value-- }
function fokusNext() { if (fokusIndex.value < bausteine.value.length - 1) fokusIndex.value++ }
// Erfahrungsleiste: Lernstand über alle Bausteine des Guides (sticky, absolviert ⊇ verstanden ⊇ gemeistert).
const fortschritt = computed(() => {
let a = 0, v = 0, g = 0
for (const s of bausteine.value) {
const l = lernstand.value[s.title] || {}
if (l.absolviert) a++
if (l.verstanden) v++
if (l.gemeistert) g++
}
return { total: bausteine.value.length, absolviert: a, verstanden: v, gemeistert: g }
})
// Cap pro Baustein folgt den erstellten Formaten: Guide vorhanden → bis 10, FullGuide → bis 25.
const tier2 = computed(() => !!props.doneByFormat?.Guide)
const tier3 = computed(() => !!props.doneByFormat?.FullGuide)
@@ -163,7 +193,7 @@ function extractContext() {
:class="['section-card', isOnePager && s.key ? 'op-card op-' + s.key : '', lernstand[s.title]?.gemeistert ? 'gemeistert' : (lernstand[s.title]?.verstanden ? 'verstanden' : (lernstand[s.title]?.absolviert ? 'absolviert' : ''))]"
:style="isOnePager && s.key ? { gridArea: s.key } : null"
>
<h3>
<h3 :class="{ 'baustein-klick': !isOnePager }" @click="!isOnePager && openFokus(s, 'pruefung')">
{{ s.title }}
<span v-if="lernstand[s.title]?.gemeistert" class="baustein-done gemeistert" title="Gemeistert (25/25)"> Gemeistert</span>
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)"> Verstanden</span>
@@ -172,14 +202,13 @@ function extractContext() {
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
<BausteinPanel
v-if="!isOnePager"
:topic="previewGuide.topic"
mode="trigger"
:baustein="s.title"
:section="s.md"
:provider="provider"
:status="lernstand[s.title]"
:tier2="tier2"
:tier3="tier3"
@status-changed="(st) => onBausteinStatus(s.title, st)"
:topic="previewGuide.topic"
@open-fokus="(tab) => openFokus(s, tab)"
/>
</article>
</div>
@@ -195,7 +224,25 @@ function extractContext() {
<p>Guide-Format anklicken um zu generieren oder Vorschau zu öffnen.</p>
</div>
<button v-if="previewGuide && !chatOpen" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
<BausteinFokus
v-if="fokusBaustein"
:baustein="fokusBaustein"
:topic="previewGuide.topic"
:provider="provider"
:fortschritt="fortschritt"
:status="lernstand[fokusBaustein.title]"
:tier2="tier2"
:tier3="tier3"
:tab="fokusTab"
:has-prev="fokusIndex > 0"
:has-next="fokusIndex < bausteine.length - 1"
@prev="fokusPrev"
@next="fokusNext"
@close="fokusIndex = null"
@status-changed="(st) => onBausteinStatus(fokusBaustein.title, st)"
/>
<button v-if="previewGuide && !chatOpen && fokusIndex === null" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
<div v-if="previewGuide && chatOpen" ref="panelEl" class="chat-panel" :class="{ shifted: elementsOpen }">
<header class="chat-header">
@@ -246,9 +293,6 @@ function extractContext() {
/* Kein horizontales Pannen der ganzen Seite — Code-Blöcke scrollen intern */
overflow-x: hidden;
background: var(--bg-preview);
/* Container-Query-Bezug fürs Baustein-Panel: misst die echte Inhaltsbreite
(ElementsSidebar schmälert sie, ohne dass die Fensterbreite sinkt). */
container-type: inline-size;
}
.guide-content {
@@ -387,63 +431,9 @@ function extractContext() {
}
}
/* Desktop: geöffnetes Baustein-Panel rechts neben dem Guide-Text statt darunter.
Greift nur, wenn ein Tab offen ist (.bp-panel existiert dann via v-if).
Tab-Buttons bleiben unten links, Panel rechts mit eigener Höhe + Scroll. */
@container (min-width: 1024px) {
.guide-content:not(.onepager) .section-card:has(.bp-panel) {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: auto auto auto; /* h3 / body / toggles */
column-gap: 1.25rem;
align-items: start;
}
.guide-content:not(.onepager) .section-card:has(.bp-panel) > h3 { grid-column: 1 / -1; grid-row: 1; }
.guide-content:not(.onepager) .section-card:has(.bp-panel) > .section-body { grid-column: 1; grid-row: 2; }
.guide-content:not(.onepager) .section-card:has(.bp-panel) :deep(.bp) { display: contents; }
.guide-content:not(.onepager) .section-card:has(.bp-panel) :deep(.bp-toggles) { grid-column: 1; grid-row: 3; }
.guide-content:not(.onepager) .section-card:has(.bp-panel) :deep(.bp-panel) {
grid-column: 2;
grid-row: 2 / 4;
margin-top: 0;
align-self: start;
/* Feste Höhe vom Viewport — nicht von der Grid-Zeile (sonst wächst es mit dem Verlauf).
sticky: bleibt beim Scrollen des langen Guide-Texts sichtbar. */
position: sticky;
top: 1rem;
height: calc(100dvh - 2rem);
display: flex;
flex-direction: column;
overflow-y: auto;
}
/* Nachrichtenliste füllt den Panel-Rest und scrollt intern. */
.guide-content:not(.onepager) .section-card:has(.bp-panel) :deep(.bp-messages) {
flex: 1;
min-height: 0;
max-height: none;
}
}
/* Breit (≥1400px Inhaltsbreite): die offene Karte bricht aus dem 880px-Lesecontainer
aus und verdoppelt sich — Text behält ~seine Breite (800px), Panel kommt rechts dazu.
cqw misst die echte guide-scroll-Breite (container-type) → schrumpft mit der
ElementsSidebar; min(...) deckelt unter overflow-x:hidden, nichts wird abgeschnitten.
Darunter (10241400) bleibt die 1fr/1fr-Halbierung von oben. */
@container (min-width: 1400px) {
.guide-scroll:has(.bp-panel) .guide-content:not(.onepager) {
max-width: min(1760px, 96cqw);
}
/* Geschlossene Karten behalten Lesebreite, zentriert in der breiten Spalte. */
.guide-content:not(.onepager):has(.bp-panel) .section-card:not(:has(.bp-panel)) {
max-width: 880px;
margin-inline: auto;
}
/* Offene Karte: Lesespalte fix ~800px, Panel füllt den Rest. */
.guide-content:not(.onepager) .section-card:has(.bp-panel) {
grid-template-columns: minmax(0, 800px) minmax(0, 1fr);
column-gap: 2.5rem;
}
}
/* Titel-Klick öffnet die Vollansicht. */
.section-card h3.baustein-klick { cursor: pointer; width: fit-content; }
.section-card h3.baustein-klick:hover { color: var(--accent); }
/* OnePager: festes 3×3-Raster über volle Breite und Höhe.
Kein Lese-Zoom (bricht 100%-Höhen) — stattdessen sind die Schriften unten 1.2× skaliert. */