update
This commit is contained in:
170
frontend/src/components/BausteinFokus.vue
Normal file
170
frontend/src/components/BausteinFokus.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<script setup>
|
||||||
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
|
import { renderMarkdown } from '../markdown.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
baustein: { type: Object, required: true }, // { title, md, num }
|
||||||
|
topic: { type: String, required: true },
|
||||||
|
provider: { type: String, default: 'claude' },
|
||||||
|
status: { type: Object, default: null },
|
||||||
|
tier2: { type: Boolean, default: false },
|
||||||
|
tier3: { type: Boolean, default: false },
|
||||||
|
tab: { type: String, default: 'pruefung' },
|
||||||
|
hasPrev: { type: Boolean, default: false },
|
||||||
|
hasNext: { type: Boolean, default: false },
|
||||||
|
fortschritt: { type: Object, default: () => ({ total: 0, absolviert: 0, verstanden: 0, gemeistert: 0 }) },
|
||||||
|
})
|
||||||
|
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineEmits(['close', 'prev', 'next', 'statusChanged'])
|
||||||
|
|
||||||
|
const pct = (n) => (100 * n / (props.fortschritt.total || 1)) + '%'
|
||||||
|
|
||||||
|
// Akzentfarbe nach Stand des aktuellen Bausteins: gemeistert=gold, verstanden=lila, absolviert=grün.
|
||||||
|
const standFarbe = computed(() => {
|
||||||
|
const s = props.status || {}
|
||||||
|
if (s.gemeistert) return '#d4af37'
|
||||||
|
if (s.verstanden) return '#8b5cf6'
|
||||||
|
if (s.absolviert) return 'var(--success-border)'
|
||||||
|
return 'var(--border)'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fokus-overlay" :style="{ '--stand': standFarbe }">
|
||||||
|
<div class="fokus-bar">
|
||||||
|
<div class="fokus-bar-inner">
|
||||||
|
<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>
|
||||||
|
<span v-if="status?.gemeistert" class="stand-badge gold">✓✓✓ Gemeistert</span>
|
||||||
|
<span v-else-if="status?.verstanden" class="stand-badge lila">✓✓ Verstanden</span>
|
||||||
|
<span v-else-if="status?.absolviert" class="stand-badge gruen">✓ Absolviert</span>
|
||||||
|
<span class="fokus-spacer"></span>
|
||||||
|
<button class="fokus-btn" title="Vollansicht beenden" @click="$emit('close')">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fokus-xp" :title="`${fortschritt.absolviert}/${fortschritt.total} absolviert · ${fortschritt.verstanden} verstanden · ${fortschritt.gemeistert} gemeistert`">
|
||||||
|
<div class="xp-seg gold" :style="{ width: pct(fortschritt.gemeistert) }"></div>
|
||||||
|
<div class="xp-seg lila" :style="{ width: pct(fortschritt.verstanden - fortschritt.gemeistert) }"></div>
|
||||||
|
<div class="xp-seg gruen" :style="{ width: pct(fortschritt.absolviert - fortschritt.verstanden) }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="fokus-body">
|
||||||
|
<div class="fokus-col left">
|
||||||
|
<div class="markdown" v-html="renderMarkdown(baustein.md)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="fokus-col right">
|
||||||
|
<BausteinPanel
|
||||||
|
mode="full"
|
||||||
|
:key="baustein.title + '|' + tab"
|
||||||
|
:initial-tab="tab"
|
||||||
|
:topic="topic"
|
||||||
|
:baustein="baustein.title"
|
||||||
|
:section="baustein.md"
|
||||||
|
:provider="provider"
|
||||||
|
:status="status"
|
||||||
|
:tier2="tier2"
|
||||||
|
:tier3="tier3"
|
||||||
|
@status-changed="$emit('statusChanged', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fokus-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 40;
|
||||||
|
/* Halbtransparent + Blur: die Guide-Liste dahinter scheint verschwommen durch die grauen Flächen. */
|
||||||
|
background: color-mix(in srgb, var(--bg-preview) 60%, transparent);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr; /* Header / XP-Leiste / Body */
|
||||||
|
}
|
||||||
|
.fokus-bar {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
/* Header-Inhalt fluchtet mit dem Body (gleiche max-width + Innenabstand). */
|
||||||
|
.fokus-bar-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 1600px;
|
||||||
|
width: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
}
|
||||||
|
.fokus-spacer { flex: 1; }
|
||||||
|
|
||||||
|
.stand-badge {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.12rem 0.6rem;
|
||||||
|
font-size: 0.72rem; font-weight: 600;
|
||||||
|
border-radius: 999px; border: 1px solid; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.stand-badge.gruen { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
|
||||||
|
.stand-badge.lila { background: color-mix(in srgb, #8b5cf6 16%, var(--panel)); border-color: #8b5cf6; color: #6d28d9; }
|
||||||
|
.stand-badge.gold { background: color-mix(in srgb, #d4af37 20%, var(--panel)); border-color: #d4af37; color: #8a6d12; }
|
||||||
|
|
||||||
|
/* Erfahrungsleiste oben: füllt sich von links — gold (gemeistert) → lila (verstanden) → grün (absolviert). */
|
||||||
|
.fokus-xp { display: flex; height: 6px; background: var(--panel-soft); }
|
||||||
|
.xp-seg { height: 100%; transition: width 0.3s ease; }
|
||||||
|
.xp-seg.gold { background: #d4af37; }
|
||||||
|
.xp-seg.lila { background: #8b5cf6; }
|
||||||
|
.xp-seg.gruen { background: var(--success-border); }
|
||||||
|
.fokus-titel { font-weight: 600; font-size: 0.95rem; margin-left: 0.5rem; }
|
||||||
|
.fokus-btn {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
min-width: 2rem; height: 2rem; padding: 0 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.fokus-btn:hover { border-color: var(--accent); }
|
||||||
|
.fokus-btn:disabled { opacity: 0.4; cursor: default; }
|
||||||
|
|
||||||
|
/* Zwei abgehobene Karten auf grauem „Schreibtisch" (--bg-preview vom Overlay). */
|
||||||
|
.fokus-body {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 1.25rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
max-width: 1600px;
|
||||||
|
width: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
.fokus-col {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* Karte kippt nach Baustein-Stand: farbiger Rahmen + getönter Hintergrund (grün/lila/gold). */
|
||||||
|
background: color-mix(in srgb, var(--stand) 7%, var(--panel));
|
||||||
|
border: 1px solid var(--stand);
|
||||||
|
border-top: 3px solid var(--stand);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
/* Links: breite Lese-Karte, Text in Lesebreite zentriert. */
|
||||||
|
.fokus-col.left { flex: 1; padding: 2rem 2.5rem; }
|
||||||
|
.fokus-col.left > * { max-width: 74ch; margin-inline: auto; }
|
||||||
|
/* Rechts: Arbeits-Karte; Panel darin randlos (Karte ist der Rahmen). */
|
||||||
|
.fokus-col.right { flex: none; width: clamp(440px, 34%, 600px); padding: 1.5rem; }
|
||||||
|
.fokus-col.right :deep(.bp) { margin-top: 0; }
|
||||||
|
.fokus-col.right :deep(.bp-panel) { border: none; background: transparent; padding: 0; }
|
||||||
|
.fokus-h2 { font-size: 1.1rem; margin: 0 0 0.75rem; }
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.fokus-body { flex-direction: column; overflow-y: auto; }
|
||||||
|
.fokus-col { overflow-y: visible; }
|
||||||
|
.fokus-col.right { width: auto; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,9 +12,13 @@ const props = defineProps({
|
|||||||
status: { type: Object, default: null }, // {gute_antworten, absolviert, verstanden, gemeistert, vertiefung}
|
status: { type: Object, default: null }, // {gute_antworten, absolviert, verstanden, gemeistert, vertiefung}
|
||||||
tier2: { type: Boolean, default: false }, // Tier 2 frei (ganzer Guide absolviert)
|
tier2: { type: Boolean, default: false }, // Tier 2 frei (ganzer Guide absolviert)
|
||||||
tier3: { type: Boolean, default: false }, // Meisterpfad frei (ganzer Guide verstanden)
|
tier3: { type: Boolean, default: false }, // Meisterpfad frei (ganzer Guide verstanden)
|
||||||
|
// 'trigger' (Liste): nur Tab-Leiste, Klick öffnet den Vollbild-Fokus.
|
||||||
|
// 'full' (Fokus): Tabs + Panel-Inhalt wie gehabt.
|
||||||
|
mode: { type: String, default: 'trigger' },
|
||||||
|
initialTab: { type: String, default: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['statusChanged'])
|
const emit = defineEmits(['statusChanged', 'openFokus'])
|
||||||
|
|
||||||
const NOETIG = 3 // absolviert
|
const NOETIG = 3 // absolviert
|
||||||
const MAX = 10 // verstanden
|
const MAX = 10 // verstanden
|
||||||
@@ -22,7 +26,13 @@ const MEISTERN = 25 // gemeistert (Maximum)
|
|||||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false, vertiefung: false, deepdive: false })
|
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false, vertiefung: false, deepdive: false })
|
||||||
|
|
||||||
// --- Toggle-Bereich ---
|
// --- Toggle-Bereich ---
|
||||||
const activeTab = ref(null) // null | 'vertiefung' | 'deepdive' | 'chat' | 'pruefung'
|
const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'vertiefung' | 'deepdive' | 'chat' | 'pruefung'
|
||||||
|
|
||||||
|
// Klick auf einen Tab: in der Liste den Fokus öffnen, im Fokus den Tab umschalten.
|
||||||
|
function tabKlick(tab) {
|
||||||
|
if (props.mode === 'trigger') { emit('openFokus', tab); return }
|
||||||
|
toggle(tab)
|
||||||
|
}
|
||||||
|
|
||||||
function toggle(tab) {
|
function toggle(tab) {
|
||||||
activeTab.value = activeTab.value === tab ? null : tab
|
activeTab.value = activeTab.value === tab ? null : tab
|
||||||
@@ -310,7 +320,12 @@ function pruefAbbrechen() {
|
|||||||
function onWindowKey(e) {
|
function onWindowKey(e) {
|
||||||
if (e.key === 'Escape' && pruefLoading.value) { e.preventDefault(); pruefAbbrechen() }
|
if (e.key === 'Escape' && pruefLoading.value) { e.preventDefault(); pruefAbbrechen() }
|
||||||
}
|
}
|
||||||
onMounted(() => window.addEventListener('keydown', onWindowKey))
|
onMounted(() => {
|
||||||
|
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
|
||||||
|
window.addEventListener('keydown', onWindowKey)
|
||||||
|
// Direkt im Fokus geöffneter Vertiefung-/Amateur-Tab: Text laden (sonst nur via toggle).
|
||||||
|
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
||||||
|
})
|
||||||
onUnmounted(() => window.removeEventListener('keydown', onWindowKey))
|
onUnmounted(() => window.removeEventListener('keydown', onWindowKey))
|
||||||
|
|
||||||
// ALT+1 Nachfragen/Nachhaken · ALT+2 primär (Antwort abgeben / Nächste Frage)
|
// ALT+1 Nachfragen/Nachhaken · ALT+2 primär (Antwort abgeben / Nächste Frage)
|
||||||
@@ -332,16 +347,16 @@ function onPruefKey(e) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="bp">
|
<div class="bp">
|
||||||
<div class="bp-toggles">
|
<div class="bp-toggles">
|
||||||
<button :class="{ active: activeTab === 'vertiefung' }" @click="toggle('vertiefung')">
|
<button :class="{ active: activeTab === 'vertiefung' }" @click="tabKlick('vertiefung')">
|
||||||
Vertiefung
|
Vertiefung
|
||||||
</button>
|
</button>
|
||||||
<button :class="{ active: activeTab === 'deepdive' }" @click="toggle('deepdive')">
|
<button :class="{ active: activeTab === 'deepdive' }" @click="tabKlick('deepdive')">
|
||||||
Amateur
|
Amateur
|
||||||
</button>
|
</button>
|
||||||
<button :class="{ active: activeTab === 'chat' }" @click="toggle('chat')">
|
<button :class="{ active: activeTab === 'chat' }" @click="tabKlick('chat')">
|
||||||
Chat
|
Chat
|
||||||
</button>
|
</button>
|
||||||
<button :class="{ active: activeTab === 'pruefung' }" @click="toggle('pruefung')">
|
<button :class="{ active: activeTab === 'pruefung' }" @click="tabKlick('pruefung')">
|
||||||
Prüfung
|
Prüfung
|
||||||
<span v-if="st.gemeistert" class="bp-chip gold" title="Gemeistert (25/25)">✓✓✓ Max</span>
|
<span v-if="st.gemeistert" class="bp-chip gold" title="Gemeistert (25/25)">✓✓✓ Max</span>
|
||||||
<span v-else-if="st.verstanden && tier3" class="bp-chip lila" title="Meisterpfad">✓✓ {{ st.gute_antworten }}/{{ MEISTERN }}</span>
|
<span v-else-if="st.verstanden && tier3" class="bp-chip lila" title="Meisterpfad">✓✓ {{ st.gute_antworten }}/{{ MEISTERN }}</span>
|
||||||
@@ -352,7 +367,7 @@ function onPruefKey(e) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeTab" class="bp-panel">
|
<div v-if="mode === 'full' && activeTab" class="bp-panel">
|
||||||
<!-- Vertiefung (gleicher Stoff, mehr) / Amateur (gleicher Stoff, für Einsteiger) -->
|
<!-- Vertiefung (gleicher Stoff, mehr) / Amateur (gleicher Stoff, für Einsteiger) -->
|
||||||
<div v-if="activeTab === 'vertiefung' || activeTab === 'deepdive'">
|
<div v-if="activeTab === 'vertiefung' || activeTab === 'deepdive'">
|
||||||
<p v-if="texte[activeTab].loading" class="bp-hint">{{ texte[activeTab].md === null ? 'Generiere…' : 'Lade…' }}</p>
|
<p v-if="texte[activeTab].loading" class="bp-hint">{{ texte[activeTab].md === null ? 'Generiere…' : 'Lade…' }}</p>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js'
|
|||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown } from '../markdown.js'
|
||||||
import { useChat } from '../composables/useChat.js'
|
import { useChat } from '../composables/useChat.js'
|
||||||
import BausteinPanel from './BausteinPanel.vue'
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
|
import BausteinFokus from './BausteinFokus.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
previewGuide: { type: Object, default: null },
|
previewGuide: { type: Object, default: null },
|
||||||
@@ -57,6 +58,35 @@ function onBausteinStatus(baustein, status) {
|
|||||||
if (status.absolviert && !warAbsolviert) emit('progressChanged') // Locks/Stats neu laden
|
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.
|
// Cap pro Baustein folgt den erstellten Formaten: Guide vorhanden → bis 10, FullGuide → bis 25.
|
||||||
const tier2 = computed(() => !!props.doneByFormat?.Guide)
|
const tier2 = computed(() => !!props.doneByFormat?.Guide)
|
||||||
const tier3 = computed(() => !!props.doneByFormat?.FullGuide)
|
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' : ''))]"
|
: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"
|
:style="isOnePager && s.key ? { gridArea: s.key } : null"
|
||||||
>
|
>
|
||||||
<h3>
|
<h3 :class="{ 'baustein-klick': !isOnePager }" @click="!isOnePager && openFokus(s, 'pruefung')">
|
||||||
{{ s.title }}
|
{{ s.title }}
|
||||||
<span v-if="lernstand[s.title]?.gemeistert" class="baustein-done gemeistert" title="Gemeistert (25/25)">✓✓✓ Gemeistert</span>
|
<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>
|
<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>
|
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
v-if="!isOnePager"
|
v-if="!isOnePager"
|
||||||
:topic="previewGuide.topic"
|
mode="trigger"
|
||||||
:baustein="s.title"
|
:baustein="s.title"
|
||||||
:section="s.md"
|
|
||||||
:provider="provider"
|
|
||||||
:status="lernstand[s.title]"
|
:status="lernstand[s.title]"
|
||||||
:tier2="tier2"
|
:tier2="tier2"
|
||||||
:tier3="tier3"
|
:tier3="tier3"
|
||||||
@status-changed="(st) => onBausteinStatus(s.title, st)"
|
:topic="previewGuide.topic"
|
||||||
|
@open-fokus="(tab) => openFokus(s, tab)"
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +224,25 @@ function extractContext() {
|
|||||||
<p>Guide-Format anklicken um zu generieren oder Vorschau zu öffnen.</p>
|
<p>Guide-Format anklicken um zu generieren oder Vorschau zu öffnen.</p>
|
||||||
</div>
|
</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 }">
|
<div v-if="previewGuide && chatOpen" ref="panelEl" class="chat-panel" :class="{ shifted: elementsOpen }">
|
||||||
<header class="chat-header">
|
<header class="chat-header">
|
||||||
@@ -246,9 +293,6 @@ function extractContext() {
|
|||||||
/* Kein horizontales Pannen der ganzen Seite — Code-Blöcke scrollen intern */
|
/* Kein horizontales Pannen der ganzen Seite — Code-Blöcke scrollen intern */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: var(--bg-preview);
|
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 {
|
.guide-content {
|
||||||
@@ -387,63 +431,9 @@ function extractContext() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop: geöffnetes Baustein-Panel rechts neben dem Guide-Text statt darunter.
|
/* Titel-Klick öffnet die Vollansicht. */
|
||||||
Greift nur, wenn ein Tab offen ist (.bp-panel existiert dann via v-if).
|
.section-card h3.baustein-klick { cursor: pointer; width: fit-content; }
|
||||||
Tab-Buttons bleiben unten links, Panel rechts mit eigener Höhe + Scroll. */
|
.section-card h3.baustein-klick:hover { color: var(--accent); }
|
||||||
@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 (1024–1400) 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* OnePager: festes 3×3-Raster über volle Breite und Höhe.
|
/* 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. */
|
Kein Lese-Zoom (bricht 100%-Höhen) — stattdessen sind die Schriften unten 1.2× skaliert. */
|
||||||
|
|||||||
Reference in New Issue
Block a user