update
This commit is contained in:
@@ -169,6 +169,11 @@ def _mathe_auftraege(text: str, wo: str) -> list[str]:
|
|||||||
if re.search(r"\\n(?![a-zA-Z])", text): # \neq etc. bleiben unberührt
|
if re.search(r"\\n(?![a-zA-Z])", text): # \neq etc. bleiben unberührt
|
||||||
auftraege.append(f"{wo}: literales „\\n“ im Text — durch echten"
|
auftraege.append(f"{wo}: literales „\\n“ im Text — durch echten"
|
||||||
f" Zeilenumbruch bzw. Leerzeile ersetzen.")
|
f" Zeilenumbruch bzw. Leerzeile ersetzen.")
|
||||||
|
lange = [m for m in re.findall(r"\$([^$\n]+?)\$", text) if len(m) > 60]
|
||||||
|
if lange: # Inline-Formeln brechen nie um — auf schmalen Screens Überlauf
|
||||||
|
auftraege.append(f"{wo}: {len(lange)} überlange Inline-Formel(n)"
|
||||||
|
f" (>60 Zeichen, z. B. „{lange[0][:40]}…“): abgesetzt"
|
||||||
|
f" als $$…$$ setzen.")
|
||||||
formeln = ([{"tex": m, "display": True} for m in re.findall(r"\$\$([\s\S]+?)\$\$", text)]
|
formeln = ([{"tex": m, "display": True} for m in re.findall(r"\$\$([\s\S]+?)\$\$", text)]
|
||||||
+ [{"tex": m, "display": False} for m in re.findall(r"\$([^$\n]+?)\$", text)])
|
+ [{"tex": m, "display": False} for m in re.findall(r"\$([^$\n]+?)\$", text)])
|
||||||
for f in _katex_fehler(formeln):
|
for f in _katex_fehler(formeln):
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ const neuName = ref('')
|
|||||||
const neuArt = ref('thema')
|
const neuArt = ref('thema')
|
||||||
const fehler = ref('')
|
const fehler = ref('')
|
||||||
const theme = ref(localStorage.getItem('theme') || 'auto')
|
const theme = ref(localStorage.getItem('theme') || 'auto')
|
||||||
const navAuf = ref(localStorage.getItem('navAuf') !== '0')
|
// mobil (≤768px) startet die Navigation eingeklappt, sofern nie manuell gesetzt
|
||||||
|
const navAuf = ref(localStorage.getItem('navAuf') !== null
|
||||||
|
? localStorage.getItem('navAuf') !== '0'
|
||||||
|
: window.innerWidth > 768)
|
||||||
|
|
||||||
function navToggle() {
|
function navToggle() {
|
||||||
navAuf.value = !navAuf.value
|
navAuf.value = !navAuf.value
|
||||||
@@ -176,9 +179,11 @@ function runBadge(run) {
|
|||||||
<button v-for="t in ['board', 'guide', 'üben', 'kennzahlen']" :key="t"
|
<button v-for="t in ['board', 'guide', 'üben', 'kennzahlen']" :key="t"
|
||||||
:class="{ aktiv: tab === t }" @click="tab = t">{{ t }}</button>
|
:class="{ aktiv: tab === t }" @click="tab = t">{{ t }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="tab === 'board'">
|
||||||
<button @click="starten" :disabled="!topic || state?.run?.status === 'running'">Start</button>
|
<button @click="starten" :disabled="!topic || state?.run?.status === 'running'">Start</button>
|
||||||
<button class="sekundaer" @click="stoppen" :disabled="state?.run?.status !== 'running'">Stop</button>
|
<button class="sekundaer" @click="stoppen" :disabled="state?.run?.status !== 'running'">Stop</button>
|
||||||
<template v-if="transferOk">
|
</template>
|
||||||
|
<template v-if="transferOk && tab === 'board'">
|
||||||
<button class="sekundaer" title="Thema auf den Server pushen (überschreibt dort)"
|
<button class="sekundaer" title="Thema auf den Server pushen (überschreibt dort)"
|
||||||
:disabled="!topic || !!uebertragung || state?.run?.status === 'running'"
|
:disabled="!topic || !!uebertragung || state?.run?.status === 'running'"
|
||||||
@click="uebertragen('push')">⇡ Push</button>
|
@click="uebertragen('push')">⇡ Push</button>
|
||||||
|
|||||||
@@ -34,10 +34,18 @@ export const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function wsVerbinden(onEvent) {
|
export function wsVerbinden(onEvent) {
|
||||||
const url = (import.meta.env.DEV ? 'ws://localhost:8000' : `ws://${location.host}`) + '/ws'
|
// Protokoll an die Seite koppeln: HTTPS erlaubt nur wss:// (Mixed Content)
|
||||||
|
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
const url = (import.meta.env.DEV ? 'ws://localhost:8000' : `${wsProto}://${location.host}`) + '/ws'
|
||||||
let ws
|
let ws
|
||||||
const verbinden = () => {
|
const verbinden = () => {
|
||||||
ws = new WebSocket(url)
|
try {
|
||||||
|
ws = new WebSocket(url)
|
||||||
|
} catch (e) {
|
||||||
|
// nie den Aufrufer mitreißen (der SecurityError killte sonst den Poll)
|
||||||
|
console.error('WebSocket nicht verfügbar:', e)
|
||||||
|
return
|
||||||
|
}
|
||||||
ws.onopen = () => onEvent({ typ: 'verbunden' }) // nach Reconnect: Stand neu laden
|
ws.onopen = () => onEvent({ typ: 'verbunden' }) // nach Reconnect: Stand neu laden
|
||||||
ws.onmessage = (e) => onEvent(JSON.parse(e.data))
|
ws.onmessage = (e) => onEvent(JSON.parse(e.data))
|
||||||
ws.onclose = () => setTimeout(verbinden, 2000)
|
ws.onclose = () => setTimeout(verbinden, 2000)
|
||||||
|
|||||||
@@ -186,11 +186,12 @@ input, select {
|
|||||||
}
|
}
|
||||||
body.vollbild .sidebar, body.vollbild .topbar { display: none; }
|
body.vollbild .sidebar, body.vollbild .topbar { display: none; }
|
||||||
body.vollbild .nav-toggle { display: none; }
|
body.vollbild .nav-toggle { display: none; }
|
||||||
/* Eingeklappt: Themen-Sidebar + Topbar weg — die Kapitelübersicht bleibt */
|
/* Eingeklappt: Themen-Sidebar + Topbar weg — die Kapitelübersicht bleibt.
|
||||||
|
Der Inhalt rückt nach unten, damit das fixe ☰ nichts überlappt. */
|
||||||
body.nav-zu .sidebar, body.nav-zu .topbar { display: none; }
|
body.nav-zu .sidebar, body.nav-zu .topbar { display: none; }
|
||||||
body.nav-zu .guide-nav { padding-top: 44px; }
|
body.nav-zu .hauptbereich { padding-top: 38px; }
|
||||||
.nav-toggle {
|
.nav-toggle {
|
||||||
position: fixed; top: 6px; left: 8px; z-index: 20;
|
position: fixed; top: 6px; left: 8px; z-index: 40; /* über den mobilen Drawern */
|
||||||
background: none; color: var(--dim); border: 0; padding: 4px 8px; font-size: 16px;
|
background: none; color: var(--dim); border: 0; padding: 4px 8px; font-size: 16px;
|
||||||
}
|
}
|
||||||
.nav-toggle:hover { color: var(--text); }
|
.nav-toggle:hover { color: var(--text); }
|
||||||
@@ -241,3 +242,32 @@ table.kennzahlen th, table.kennzahlen td {
|
|||||||
}
|
}
|
||||||
table.kennzahlen th:first-child, table.kennzahlen td:first-child { text-align: left; }
|
table.kennzahlen th:first-child, table.kennzahlen td:first-child { text-align: left; }
|
||||||
table.kennzahlen th { color: var(--dim); }
|
table.kennzahlen th { color: var(--dim); }
|
||||||
|
|
||||||
|
/* ── Mobil (≤768px): Navigationen als Drawer, Board mit Spalten-Snap ────────── */
|
||||||
|
.markdown .katex { max-width: 100%; overflow-x: auto; overflow-y: hidden; }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar, .guide-nav {
|
||||||
|
position: fixed; top: 0; bottom: 0; left: 0; z-index: 30;
|
||||||
|
width: min(85vw, 320px); min-width: 0;
|
||||||
|
border-right: 1px solid var(--rand); box-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
/* Guide-Tab: der Drawer zeigt die Kapitelliste, sonst die Themenliste */
|
||||||
|
body:has(.guide-nav) .sidebar { display: none; }
|
||||||
|
/* mobil versteckt ☰ nur die Drawer — die Topbar (Tabs!) bleibt sichtbar */
|
||||||
|
body.nav-zu .topbar { display: flex; }
|
||||||
|
body.nav-zu .hauptbereich { padding-top: 0; }
|
||||||
|
/* mobil heißt ☰-zu: ALLE Drawer weg — auch die Kapitelnav (Desktop behält sie) */
|
||||||
|
body.nav-zu .guide-nav { display: none; }
|
||||||
|
.sidebar .topics { padding-top: 44px; }
|
||||||
|
.guide-nav { padding-top: 44px; }
|
||||||
|
.topbar { padding-left: 40px; gap: 6px; }
|
||||||
|
.topbar .badge { display: none; }
|
||||||
|
.board { scroll-snap-type: x mandatory; padding: 8px; gap: 8px; }
|
||||||
|
.spalte { min-width: 86vw; max-width: 86vw; scroll-snap-align: start; }
|
||||||
|
.seite { padding: 12px; }
|
||||||
|
.seite.guide-text { max-width: 100%; padding: 14px 16px; }
|
||||||
|
.guide-section { padding: 12px 14px; }
|
||||||
|
.guide-text .kapitel-titel { margin-top: 20px; }
|
||||||
|
.flash { max-width: 100%; padding: 18px; margin: 16px auto; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ Regeln (hart):
|
|||||||
Nur KaTeX-STANDARDBEFEHLE — kein LaTeX-Textsatz wie `\textsc`, `\usepackage`.
|
Nur KaTeX-STANDARDBEFEHLE — kein LaTeX-Textsatz wie `\textsc`, `\usepackage`.
|
||||||
Ein Parse-Gate prüft jede Formel mit dem echten Renderer.
|
Ein Parse-Gate prüft jede Formel mit dem echten Renderer.
|
||||||
Hervorhebung im Text NUR als `**fett**` — nie einzelne `*…*`.
|
Hervorhebung im Text NUR als `**fett**` — nie einzelne `*…*`.
|
||||||
|
Formeln über ~50 Zeichen ABGESETZT als `$$…$$` — lange Inline-Formeln laufen
|
||||||
|
auf schmalen Bildschirmen aus dem Text.
|
||||||
PDF-Artefakte aus den Belegen in saubere Notation übersetzen: „u 6= v" ist
|
PDF-Artefakte aus den Belegen in saubere Notation übersetzen: „u 6= v" ist
|
||||||
$u \neq v$, „V 0" ist $V'$ — du lehrst, du zitierst nicht.
|
$u \neq v$, „V 0" ist $V'$ — du lehrst, du zitierst nicht.
|
||||||
5. LÄNGE: „lang" hat {min_woerter}–{max_woerter} Wörter. Die Obergrenze ist ein HARTES
|
5. LÄNGE: „lang" hat {min_woerter}–{max_woerter} Wörter. Die Obergrenze ist ein HARTES
|
||||||
|
|||||||
@@ -203,3 +203,5 @@ def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
|||||||
assert "Beträge" in auftraege
|
assert "Beträge" in auftraege
|
||||||
assert "einzelnes „*“" in auftraege
|
assert "einzelnes „*“" in auftraege
|
||||||
assert "literales" in auftraege
|
assert "literales" in auftraege
|
||||||
|
lang_formel = "Es gilt $" + "x + ".join(["y"] * 30) + "$ hier."
|
||||||
|
assert "überlange Inline-Formel" in " ".join(guide._mathe_auftraege(lang_formel, "T"))
|
||||||
|
|||||||
Reference in New Issue
Block a user