Einheitliche Sim-Kopfzeile (Entwurf B) als Komponente + Lizenz-Datenmodell

- ggs-header.css/.js: schwebende Pillen-Kopfzeile aus vorhandenem
  .ggs-header-Markup (Kreis-Logo/Titel/Level als eine Pille, eigene
  SVG-Icons, Tempo-Panel links + Zustandsbutton, Musik-Panel unten,
  30% transparente Untermenues, Glossar target=_blank, Lehrplan raus).
  Enhance-Ansatz: Spiel-Logik (IDs/Klassen, data-speed) bleibt unberuehrt.
- Eingebaut in Klima 2D/3D, Fluss, Logistik (Auto-Tempo), Energiemanager,
  Kofferdetektiv 3D.
- admin-licenses.html: Datenmodell-Sektion (licenses-Tabelle, Lebenszyklus
  Generiert->Eingeloest->Zugewiesen, Beziehungen, Jahr->Schuljahr->Code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 14:06:41 +02:00
parent 362aaba883
commit 502f271f36
9 changed files with 601 additions and 6 deletions
+94
View File
@@ -35,6 +35,33 @@
.toolbar{display:flex;gap:.4rem;margin-bottom:.8rem;flex-wrap:wrap}
.msg{font-size:.72rem;padding:.4rem .6rem;border-radius:6px;margin-top:.5rem}
.msg.ok{background:#dceadd;color:#5a8a5e}
/* ---- Datenmodell-Sektion ---- */
.dm-lead{font-size:.74rem;color:#4a4a4a;line-height:1.55;margin-bottom:.6rem}
.dm-sub{font-size:.6rem;font-weight:800;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin:1.2rem 0 .5rem}
.dm-note{font-size:.66rem;color:#8a6a2a;background:#fdf6e3;border:1px solid #efe0b8;border-radius:6px;padding:.45rem .6rem;margin-top:.5rem;line-height:1.5}
.dm-flow{display:flex;gap:.4rem;align-items:stretch;flex-wrap:wrap}
.dm-step{flex:1;min-width:190px;background:#f7f6f3;border-radius:8px;border-left:4px solid #c4a97a;padding:.65rem .75rem}
.dm-step.s-pool{border-left-color:#b8b2a6}
.dm-step.s-redeem{border-left-color:#4a7c8a}
.dm-step.s-assign{border-left-color:#5a8a5e}
.dm-step .s-top{display:flex;align-items:center;gap:.4rem;margin-bottom:.35rem}
.dm-step .s-n{width:18px;height:18px;border-radius:50%;background:#4a7c8a;color:#fff;font-size:.62rem;font-weight:800;display:flex;align-items:center;justify-content:center;flex:none}
.dm-step.s-pool .s-n{background:#b8b2a6}
.dm-step.s-assign .s-n{background:#5a8a5e}
.dm-step .s-t{font-size:.74rem;font-weight:800;color:#2f3b3f}
.dm-step .s-who{font-size:.6rem;font-weight:700;color:#8a8a8a;text-transform:uppercase;letter-spacing:.03em}
.dm-step .s-d{font-size:.68rem;color:#4a4a4a;line-height:1.45;margin-top:.15rem}
.dm-step code{font-family:'Courier New',monospace;font-size:.66rem;background:rgba(0,0,0,.05);padding:.05rem .3rem;border-radius:4px;color:#c85c4a}
.dm-arrow{display:flex;align-items:center;justify-content:center;color:#c4a97a;font-size:1.4rem;font-weight:900;padding:0 .1rem}
.dm-er{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;font-size:.68rem}
.dm-ent{background:#fff;border:1.5px solid #4a7c8a;color:#4a7c8a;border-radius:6px;padding:.3rem .55rem;font-weight:800;font-family:'Courier New',monospace}
.dm-ent.e-lic{background:#4a7c8a;color:#fff}
.dm-rel{color:#8a8a8a;display:flex;flex-direction:column;align-items:center;line-height:1;font-family:'Courier New',monospace;font-size:.58rem}
.dm-rel b{color:#c4a97a;font-size:1.1rem}
.dm-derive{background:#f7f6f3;border-radius:8px;padding:.7rem .8rem;font-size:.7rem;line-height:1.7}
.dm-derive .d-code{font-family:'Courier New',monospace;font-weight:800;color:#4a7c8a;letter-spacing:.04em}
.dm-derive .d-hi{background:#e8c547;color:#4a4a4a;padding:.02rem .28rem;border-radius:4px;font-weight:800}
td.mono{font-family:'Courier New',monospace;font-size:.68rem;color:#c85c4a}
</style>
</head>
<body>
@@ -104,6 +131,73 @@
<button class="btn btn-outline" id="btn-next" onclick="nextPage()">Weiter →</button>
</div>
</div>
<!-- ============ Datenmodell-Referenz ============ -->
<div class="card">
<div class="card-h">📐 Datenmodell — Lizenznummern</div>
<p class="dm-lead">Eine Lizenz = <b>ein Sitzplatz für eine:n Schüler:in in einem Schuljahr</b>. Der Admin erzeugt einen Vorrat an Codes, eine Lehrperson <b>löst</b> Codes ein (sie werden ihrem Konto zugeordnet) und <b>weist</b> sie dann einzelnen Schüler:innen zu. Alles steht in <b>einer</b> Tabelle <span class="code">licenses</span> — die drei Zustände ergeben sich allein aus zwei Feldern: <span class="code">teacher_id</span> und <span class="code">student_id</span>.</p>
<div class="dm-sub">Tabelle „licenses"</div>
<table>
<tr><th>Spalte</th><th>Typ</th><th>Bedeutung</th></tr>
<tr><td class="mono">id</td><td>INT · PK</td><td>Interne ID</td></tr>
<tr><td class="mono">code</td><td>VARCHAR · UNIQUE</td><td>Der Lizenzcode, Format <span class="code">XXXX-XXXX-XXXX-JAHR</span></td></tr>
<tr><td class="mono">school_year</td><td>VARCHAR</td><td>Schuljahr, z.&nbsp;B. „26/27" — aus dem Jahr abgeleitet</td></tr>
<tr><td class="mono">teacher_id</td><td>FK → teachers</td><td>Einlösende Lehrperson · <b>NULL = noch im Pool</b></td></tr>
<tr><td class="mono">student_id</td><td>FK → students</td><td>Platz-Inhaber:in · <b>NULL = noch frei</b></td></tr>
<tr><td class="mono">redeemed_at</td><td>TIMESTAMP · NULL</td><td>Zeitpunkt des Einlösens durch die Lehrperson</td></tr>
<tr><td class="mono">created_at</td><td>TIMESTAMP</td><td>Erstellung (Admin-Generierung)</td></tr>
</table>
<div class="dm-sub">Lebenszyklus &amp; Einlösen</div>
<div class="dm-flow">
<div class="dm-step s-pool">
<div class="s-top"><span class="s-n">1</span><span class="s-t">Generiert</span></div>
<div class="s-who">Admin</div>
<div class="s-d"><code>teacher_id</code> = NULL · <code>student_id</code> = NULL. Frisch erzeugter Code im Pool.<br>Aktion: <code>generate</code></div>
</div>
<div class="dm-arrow"></div>
<div class="dm-step s-redeem">
<div class="s-top"><span class="s-n">2</span><span class="s-t">Eingelöst</span></div>
<div class="s-who">Lehrperson</div>
<div class="s-d"><code>teacher_id</code> gesetzt · <code>redeemed_at</code> = jetzt · <code>student_id</code> noch NULL. Code gehört nun der Lehrperson.<br>Aktion: <code>redeem</code> / <code>redeem_batch</code></div>
</div>
<div class="dm-arrow"></div>
<div class="dm-step s-assign">
<div class="s-top"><span class="s-n">3</span><span class="s-t">Zugewiesen</span></div>
<div class="s-who">Lehrperson</div>
<div class="s-d"><code>student_id</code> gesetzt. Der Platz ist von einer konkreten Person belegt.<br>Aktion: <code>assign</code> / <code>auto_assign</code></div>
</div>
</div>
<div class="dm-note">⚠️ In der Statistik oben zählt <b>„Frei"</b> = <span class="code">student_id IS NULL</span> — also auch schon <b>eingelöste</b>, aber noch nicht zugewiesene Codes. <b>„Zugewiesen"</b> = <span class="code">student_id</span> gesetzt. Ob ein freier Code schon einer Lehrperson gehört, sieht man an <span class="code">teacher_id</span> / <span class="code">redeemed_at</span>.</div>
<div class="dm-sub">Beziehungen</div>
<div class="dm-er" style="margin-bottom:.5rem">
<span class="dm-ent e-lic">licenses</span>
<span class="dm-rel"><b></b>teacher_id</span>
<span class="dm-ent">teachers</span>
</div>
<div class="dm-er">
<span class="dm-ent e-lic">licenses</span>
<span class="dm-rel"><b></b>student_id</span>
<span class="dm-ent">students</span>
<span class="dm-rel"><b></b>class_id</span>
<span class="dm-ent">classes</span>
<span class="dm-rel"><b></b>teacher_id</span>
<span class="dm-ent">teachers</span>
</div>
<p class="dm-lead" style="margin-top:.5rem"><span class="code">licenses.teacher_id</span> merkt sich die <b>einlösende</b> Lehrperson, <span class="code">licenses.student_id</span> den <b>Platz-Inhaber</b>. Über <span class="code">students.class_id → classes.teacher_id</span> hängt jede:r Schüler:in ohnehin an einer Klasse einer Lehrperson — der direkte <span class="code">teacher_id</span>-Eintrag macht aber auch <b>eingelöste, noch nicht zugewiesene</b> Codes eindeutig einer Lehrperson zuordenbar.</p>
<div class="dm-sub">Jahr → Schuljahr → Code</div>
<div class="dm-derive">
Eingabe beim Generieren: <b>Jahr</b> = <span class="d-code">2026</span> (Kalenderjahr, in dem das Schuljahr <b>beginnt</b>).<br>
<b>school_year</b> = „letzte 2 Ziffern" &nbsp;<span class="d-code">26</span>&nbsp; / &nbsp;<span class="d-code">26+1</span>&nbsp; = <span class="d-hi">26/27</span><br>
<b>Code-Suffix</b> = das volle Jahr: <span class="d-code">-2026</span><br>
<b>Zeichen-Alphabet</b> (ohne Verwechsler): <span class="code">AZ ohne I,O</span> + <span class="code">29</span> (kein 0, kein 1)<br>
→ 3 Blöcke à 4 Zeichen + Jahr &nbsp;&nbsp; Beispiel: <span class="d-code">K7QF-3M9P-RT2W-2026</span> &nbsp;·&nbsp; Schuljahr <span class="d-hi">26/27</span>
</div>
<div class="dm-note" style="background:#eef4f6;border-color:#cfe0e6;color:#3a5b64">Das <b>Jahr</b> steckt fest im Code (Suffix) und bestimmt <b>school_year</b>. Für ein neues Schuljahr wird ein neuer Vorrat generiert; Filter/Export laufen über <span class="code">school_year</span>.</div>
</div>
</div>
<script>
+215
View File
@@ -0,0 +1,215 @@
/* ============================================================
GGS-HEADER — Einheitliche Sim-Kopfzeile (Entwurf B, „schwebend")
------------------------------------------------------------
Wird von ggs-header.js aktiviert (Klasse .ggs-header--float auf
<header class="ggs-header">). Überführt das vorhandene
Design-System-Markup in die schwebende Pillen-Optik:
Kreis-Logo · Titel · Level = EINE Pille, rechts schwebende
Icon-Buttons, Tempo-/Musik-Panels als Untermenü (30 % transparent).
Styling ist auf .ggs-header--float gescoped → betrifft nur
Sims, die die Komponente laden.
============================================================ */
/* Werte 1:1 aus admin-styleguide.html (Entwurf B, von Thomas bestätigt) */
.ggs-header--float {
background: transparent !important;
border-bottom: none !important;
height: auto !important;
min-height: 50px;
gap: .5rem !important;
padding: .55rem .7rem !important;
position: relative;
z-index: 1000;
flex-wrap: nowrap;
}
/* --- Marken-Pille: Kreis-Logo · Titel · Level ---------------- */
.ggs-header--float .ggs-hpille {
display: flex;
align-items: center;
gap: .5rem;
height: 38px;
padding: 0 .8rem 0 .3rem;
background: rgba(255, 255, 255, .96);
border-radius: 19px;
box-shadow: 0 2px 9px rgba(0, 0, 0, .2);
flex: none;
}
.ggs-header--float .ggs-header-logo {
width: 30px;
height: 30px;
border-radius: 50%;
background: #1f4e5a;
flex: none;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
gap: 0;
}
.ggs-header--float .ggs-header-logo-icon {
width: 100% !important;
height: 100% !important;
object-fit: cover;
animation: none !important;
}
.ggs-header--float .ggs-header-logo-text { display: none !important; }
.ggs-header--float .ggs-header-separator { display: none !important; }
.ggs-header--float .ggs-header-module {
font-weight: 800;
color: #1f4e5a;
font-size: .92rem;
white-space: nowrap;
line-height: 1;
}
.ggs-header--float .ggs-header-badge {
font-size: .6rem;
font-weight: 800;
color: #fff;
background: #5a8a5e;
border-radius: 9px;
padding: .2rem .5rem;
line-height: 1;
white-space: nowrap;
}
/* Live-Tick-Punkt (klima) darf in der Pille mitlaufen */
.ggs-header--float .ggs-hpille .kw-tick-dot { flex: none; }
/* --- Schwebende Icon-Buttons (rechts + Home links) ----------- */
.ggs-header--float .ggs-btn-ghost,
.ggs-header--float .ggs-spd,
.ggs-header--float .ggs-mtrigger {
width: 38px;
height: 38px;
border-radius: 12px;
border: none;
background: rgba(255, 255, 255, .96);
box-shadow: 0 2px 9px rgba(0, 0, 0, .2);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #1f4e5a;
padding: 0;
flex: none;
text-decoration: none;
transition: background .15s, transform .1s;
}
.ggs-header--float .ggs-btn-ghost:hover,
.ggs-header--float .ggs-spd:hover,
.ggs-header--float .ggs-mtrigger:hover {
background: #fff;
color: #16323a;
border-radius: 12px;
transform: translateY(-1px);
}
.ggs-header--float .ggs-btn-ghost .ic,
.ggs-header--float .ggs-mtrigger .ic {
width: 20px;
height: 20px;
}
.ggs-header--float .ggs-header-spacer { flex: 1; }
.ggs-header--float .ggs-header-actions {
display: flex;
align-items: center;
gap: .5rem;
flex: none;
}
/* --- Tempo-Zustandsbutton (zeigt IMMER den aktuellen Speed) --- */
.ggs-header--float .ggs-tempo { position: relative; display: inline-flex; }
.ggs-header--float .ggs-spd {
flex-direction: column;
line-height: 1;
font-weight: 800;
}
.ggs-header--float .ggs-spd b { font-size: .82rem; }
.ggs-header--float .ggs-spd small {
font-size: .5rem;
font-weight: 700;
margin-top: 1px;
color: #4a7c8a;
letter-spacing: .02em;
}
.ggs-header--float .ggs-spd .ic { width: 18px; height: 18px; color: inherit; }
.ggs-header--float .ggs-spd.act { background: #4a7c8a; color: #fff; }
.ggs-header--float .ggs-spd.act small { color: #dbe9ee; }
/* --- Untermenü-Panels (Tempo links, Musik unten) ------------ */
.ggs-header--float .ggs-pop {
display: none;
align-items: center;
gap: .25rem;
height: 38px;
padding: 0 .3rem;
background: rgba(255, 255, 255, .7);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
border-radius: 12px;
box-shadow: 0 2px 9px rgba(0, 0, 0, .14);
border: 1px solid rgba(255, 255, 255, .5);
position: absolute;
z-index: 1001;
}
.ggs-header--float .ggs-tempo.open .ggs-pop--tempo,
.ggs-header--float .ggs-music-wrap.open .ggs-pop--music { display: inline-flex; }
/* Tempo: öffnet LINKS vom Button, bleibt offen */
.ggs-header--float .ggs-pop--tempo {
right: calc(100% + 6px);
top: 50%;
transform: translateY(-50%);
}
/* Musik: klappt nach UNTEN, schließt nach Song-Auswahl */
.ggs-header--float .ggs-music-wrap { position: relative; display: inline-flex; }
.ggs-header--float .ggs-pop--music {
top: calc(100% + 6px);
right: 0;
gap: .5rem;
padding: 0 .55rem;
height: 38px;
}
/* --- Tempo-Segmente im Panel (nutzt bestehende .ggs-speed) --- */
.ggs-header--float .ggs-pop--tempo .ggs-speed {
display: inline-flex;
gap: .25rem;
border: none;
background: transparent;
box-shadow: none;
border-radius: 0;
overflow: visible;
}
.ggs-header--float .ggs-pop--tempo .ggs-speed-btn {
height: 30px;
min-width: 36px;
padding: 0 .6rem;
border: none !important;
background: transparent;
border-radius: 8px;
font-weight: 800;
font-size: .82rem;
color: #4a4a4a;
display: inline-flex;
align-items: center;
justify-content: center;
}
.ggs-header--float .ggs-pop--tempo .ggs-speed-btn:hover { background: rgba(74, 124, 138, .15); color: #1f4e5a; }
.ggs-header--float .ggs-pop--tempo .ggs-speed-btn.active { background: #4a7c8a; color: #fff; }
/* --- Musik-Widget im Panel (bestehendes #ggs-music) --------- */
.ggs-header--float .ggs-pop--music .ggs-music {
display: inline-flex;
align-items: center;
gap: .5rem;
background: transparent;
box-shadow: none;
border: none;
padding: 0;
height: auto;
}
.ggs-header--float .ggs-pop--music .ggs-music[hidden] { display: inline-flex !important; }
/* Schmaler auf iPad-Landscape (Platz sparen) */
@media (max-width: 1100px) {
.ggs-header--float .ggs-header-actions { gap: .4rem; }
}
+274
View File
@@ -0,0 +1,274 @@
/* ============================================================
GGS-HEADER — Einheitliche Sim-Kopfzeile (Entwurf B)
------------------------------------------------------------
Überführt das vorhandene <header class="ggs-header">-Markup
(Design-System) in die schwebende Pillen-Optik OHNE die
Spiel-Logik anzufassen: bestehende IDs/Klassen (#btn-sound,
#btn-reset, [data-speed], #level-badge, #ggs-music …) bleiben
erhalten, die Handler der Sim greifen weiter.
Nutzung: einfach ggs-header.css + ggs-header.js einbinden.
Auto-Init auf DOMContentLoaded für jedes .ggs-header.
Optionen über data-Attribute am <header>:
data-ggs-home Home-Ziel (Default: vorh. Home-Link / "/schueler")
data-ggs-glossar Glossar-URL (Default: "/glossar")
data-ggs-sim Sim-ID (für /glossar?module=<id>)
data-ggs-no-glossar Glossar-Button weglassen
============================================================ */
(function () {
'use strict';
var SPRITE_ID = 'ggs-header-sprite';
var ICONS = {
home: '<path d="M4 11.4 12 4l8 7.4"/><path d="M6 10.3V19a1 1 0 0 0 1 1h3v-5h4v5h3a1 1 0 0 0 1-1v-8.7"/>',
tempo: '<path d="M4.6 16.5a7.5 7.5 0 0 1 14.8 0"/><path d="M12 16 15.2 12"/><circle cx="12" cy="16.3" r="1.3" fill="currentColor" stroke="none"/>',
music: '<path d="M9 17V6.4l9-1.8V15"/><circle cx="6.5" cy="17" r="2.4"/><circle cx="15.5" cy="15" r="2.4"/>',
sound: '<path d="M4 9.5h3.2L11 6v12l-3.8-3.5H4z"/><path d="M14.6 9.6a4 4 0 0 1 0 4.8"/><path d="M17 7.4a7 7 0 0 1 0 9.2"/>',
'sound-off': '<path d="M4 9.5h3.2L11 6v12l-3.8-3.5H4z"/><path d="M15.5 10l4 4"/><path d="M19.5 10l-4 4"/>',
restart: '<path d="M19 12a7 7 0 1 1-2-4.9"/><path d="M19 4.5V9h-4.5"/>',
help: '<circle cx="12" cy="12" r="8.4"/><path d="M9.7 9.3a2.4 2.4 0 0 1 4.6.8c0 1.6-2.3 1.8-2.3 3.2"/><circle cx="12" cy="16.4" r=".7" fill="currentColor" stroke="none"/>',
glossar: '<path d="M12 6.6C10 5.2 7 5.2 5 6.2v11c2-1 5-1 7 .4"/><path d="M12 6.6c2-1.4 5-1.4 7-.4v11c-2-1-5-1-7 .4"/><path d="M12 6.6v11.4"/>',
pause: '<rect x="6" y="5" width="4" height="14" rx="1.2" fill="currentColor"/><rect x="14" y="5" width="4" height="14" rx="1.2" fill="currentColor"/>',
caret: '<path d="M7 10l5 5 5-5"/>'
};
/* SVG-Sprite einmalig ins Dokument legen */
function injectSprite() {
if (document.getElementById(SPRITE_ID)) return;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('id', SPRITE_ID);
svg.setAttribute('width', '0');
svg.setAttribute('height', '0');
svg.setAttribute('aria-hidden', 'true');
svg.style.position = 'absolute';
var defs = '';
for (var key in ICONS) {
var stroked = (key === 'pause');
defs += '<symbol id="ggic-' + key + '" viewBox="0 0 24 24"' +
(stroked ? '' : ' fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"') +
'>' + ICONS[key] + '</symbol>';
}
svg.innerHTML = '<defs>' + defs + '</defs>';
document.body.insertBefore(svg, document.body.firstChild);
}
function icon(name, cls) {
return '<svg class="ic ' + (cls || '') + '" aria-hidden="true"><use href="#ggic-' + name + '"/></svg>';
}
function setIcon(el, name, title) {
if (!el) return;
el.innerHTML = icon(name);
if (title) el.setAttribute('title', title);
}
/* Ersten passenden Button per ID-Liste finden (bespoke IDs abdecken) */
function byIds(scope, ids) {
for (var i = 0; i < ids.length; i++) {
var el = scope.querySelector('#' + ids[i]);
if (el) return el;
}
return null;
}
var SOUND_IDS = ['btn-sound', 'btn-mute', 'btnVoiceToggle', 'btnAudioToggle', 'btnAudio', 'btn-ton', 'btn-audio'];
var RESET_IDS = ['btn-reset', 'btnReset', 'btn-restart', 'btn-neustart'];
var HELP_IDS = ['btn-info', 'btnHelp', 'btn-help', 'btn-anleitung', 'btn-hilfe'];
/* Glyph-Buttons (Ton), die die Sim per textContent umschaltet:
Icon nach jeder Änderung nachziehen, Mute-Zustand als Icon spiegeln. */
function watchGlyphBtn(btn, mapFn, title) {
if (!btn) return;
if (title) btn.setAttribute('title', title);
var apply = function () {
if (btn.querySelector('svg.ic')) return; // schon Icon → nichts tun
setIcon(btn, mapFn(btn.textContent || ''));
};
apply();
new MutationObserver(apply).observe(btn, { childList: true, characterData: true, subtree: true });
}
/* ------------------------------------------------------------ */
function enhance(header, opts) {
if (!header || header.hasAttribute('data-ggs-enhanced')) return;
opts = opts || {};
injectSprite();
header.setAttribute('data-ggs-enhanced', '1');
header.classList.add('ggs-header--float');
var q = function (sel) { return header.querySelector(sel); };
var homeHref = opts.home || header.getAttribute('data-ggs-home') || '/schueler';
var simId = opts.sim || header.getAttribute('data-ggs-sim') || '';
var glossarHref = opts.glossar || header.getAttribute('data-ggs-glossar') ||
('/glossar' + (simId ? '?module=' + encodeURIComponent(simId) : ''));
var actions = q('.ggs-header-actions');
/* --- 1) Marken-Pille: Logo · Titel · Level ---------------- */
var logo = q('.ggs-header-logo');
var pille = document.createElement('div');
pille.className = 'ggs-hpille';
if (logo) {
logo.parentNode.insertBefore(pille, logo);
pille.appendChild(logo);
} else {
header.insertBefore(pille, header.firstChild);
}
var mod = q('.ggs-header-module'); if (mod) pille.appendChild(mod);
var badges = header.querySelectorAll('.ggs-header-badge');
for (var i = 0; i < badges.length; i++) pille.appendChild(badges[i]);
var tick = header.querySelector('.kw-tick-dot'); if (tick) pille.appendChild(tick);
/* --- 2) Home-Button links neben die Pille ----------------- */
var homeBtn = null;
var acts = header.querySelectorAll('.ggs-header-actions a, .ggs-header-actions button');
for (var h = 0; h < acts.length; h++) {
var a = acts[h];
var href = a.getAttribute('href') || '';
if (a.id === 'btn-info' || a.id === 'btn-sound' || a.id === 'btn-reset' || a.id === 'ggs-lehrplan-link') continue;
if (/schueler|cockpit|\/$/.test(href) || (a.textContent || '').indexOf('🏠') >= 0) { homeBtn = a; break; }
}
if (!homeBtn) {
homeBtn = document.createElement('a');
if (actions) actions.appendChild(homeBtn);
}
homeBtn.className = 'ggs-btn-ghost';
if (!homeBtn.getAttribute('href')) homeBtn.setAttribute('href', homeHref);
setIcon(homeBtn, 'home', 'Startseite');
header.insertBefore(homeBtn, pille.nextSibling);
/* Spacer sicherstellen (zwischen Home-Gruppe und Aktionen) */
var spacer = q('.ggs-header-spacer');
if (!spacer) {
spacer = document.createElement('div');
spacer.className = 'ggs-header-spacer';
}
header.insertBefore(spacer, actions);
/* --- 3) Aktions-Icons setzen (bespoke IDs via Listen) ----- */
var helpBtn = byIds(header, HELP_IDS);
var soundBtn = byIds(header, SOUND_IDS);
var resetBtn = byIds(header, RESET_IDS);
setIcon(helpBtn, 'help', 'Hilfe / Anleitung');
watchGlyphBtn(soundBtn, function (t) { return /🔇|🔈|🔕|off|aus|mute|stumm/i.test(t) ? 'sound-off' : 'sound'; }, 'Ton an/aus');
setIcon(resetBtn, 'restart', 'Neu starten');
var lehrplan = q('#ggs-lehrplan-link'); if (lehrplan) lehrplan.style.display = 'none';
/* --- 4) Tempo-Panel (nur wenn .ggs-speed vorhanden) ------- */
var speed = q('.ggs-speed');
var tempoWrap = null, spdBtn = null;
if (speed) {
tempoWrap = document.createElement('div');
tempoWrap.className = 'ggs-tempo';
spdBtn = document.createElement('button');
spdBtn.className = 'ggs-spd';
spdBtn.type = 'button';
spdBtn.setAttribute('title', 'Tempo');
var pop = document.createElement('div');
pop.className = 'ggs-pop ggs-pop--tempo';
speed.parentNode.insertBefore(tempoWrap, speed);
pop.appendChild(speed);
tempoWrap.appendChild(pop);
tempoWrap.appendChild(spdBtn);
var syncSpeed = function () {
var active = speed.querySelector('.ggs-speed-btn.active') || speed.querySelector('.ggs-speed-btn[data-speed="1"]');
if (!active) return;
var sp = active.getAttribute('data-speed'); // klassische Speed-Buttons
var txt = (active.textContent || '').replace(/\s+/g, ' ').trim();
var isAuto = /auto/i.test(active.getAttribute('title') || '') || active.hasAttribute('data-auto') || active.getAttribute('data-mode') === 'auto';
var isPause = sp === '0' || active.getAttribute('data-mode') === 'pause' || /^\s*(pause|⏸)/i.test(txt);
if (isPause) {
spdBtn.innerHTML = icon('pause');
} else if (isAuto) {
// Auto-Label (z. B. „Auto 1×") steht schon voll im Text → nicht doppeln
spdBtn.innerHTML = /auto/i.test(txt) ? '<b>' + txt + '</b>' : '<b>' + (txt || 'Auto') + '</b><small>Auto</small>';
} else {
spdBtn.innerHTML = '<b>' + (txt || (sp + '×')) + '</b>';
}
spdBtn.classList.toggle('act', isPause || isAuto || (sp !== null && sp !== '1'));
};
syncSpeed();
// Klick auf ein Segment: Sim-Handler läuft weiter, Label spiegeln, Panel bleibt offen
var spBtns = speed.querySelectorAll('.ggs-speed-btn');
for (var s = 0; s < spBtns.length; s++) {
spBtns[s].addEventListener('click', function () { setTimeout(syncSpeed, 0); });
}
// Ein Observer über den ganzen Speed-Block: fängt Klassen-Flip UND Text-Änderung (Auto 1×↔8×)
var mo = new MutationObserver(syncSpeed);
mo.observe(speed, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['class'] });
spdBtn.addEventListener('click', function (e) {
e.stopPropagation();
tempoWrap.classList.toggle('open');
});
}
/* --- 5) Musik-Panel (nur wenn #ggs-music vorhanden) ------- */
var music = q('#ggs-music');
var musicWrap = null;
if (music) {
musicWrap = document.createElement('div');
musicWrap.className = 'ggs-music-wrap';
var mtrigger = document.createElement('button');
mtrigger.className = 'ggs-mtrigger';
mtrigger.type = 'button';
mtrigger.setAttribute('title', 'Musik');
mtrigger.innerHTML = icon('music');
var mpop = document.createElement('div');
mpop.className = 'ggs-pop ggs-pop--music';
music.parentNode.insertBefore(musicWrap, music);
music.removeAttribute('hidden');
mpop.appendChild(music);
musicWrap.appendChild(mtrigger);
musicWrap.appendChild(mpop);
mtrigger.addEventListener('click', function (e) {
e.stopPropagation();
musicWrap.classList.toggle('open');
});
// Nach Song-Auswahl Panel schließen
var sel = music.querySelector('#ggs-music-sel, select');
if (sel) sel.addEventListener('change', function () { musicWrap.classList.remove('open'); });
}
/* --- 6) Glossar-Button (target=_blank) -------------------- */
if (!opts.noGlossar && !header.hasAttribute('data-ggs-no-glossar') && actions) {
var gl = document.createElement('a');
gl.className = 'ggs-btn-ghost ggs-glossar';
gl.setAttribute('href', glossarHref);
gl.setAttribute('target', '_blank');
gl.setAttribute('rel', 'noopener');
setIcon(gl, 'glossar', 'Glossar (neuer Tab)');
actions.appendChild(gl);
}
/* --- 7) Aktionen in Mockup-Reihenfolge sortieren ---------- */
if (actions) {
var order = [tempoWrap, musicWrap, soundBtn, resetBtn, helpBtn, q('.ggs-glossar')];
for (var o = 0; o < order.length; o++) if (order[o]) actions.appendChild(order[o]);
}
/* --- 8) Panels bei Außen-Klick schließen ------------------ */
document.addEventListener('click', function (e) {
if (musicWrap && !musicWrap.contains(e.target)) musicWrap.classList.remove('open');
// Tempo bleibt bewusst offen (Toggle) — nur Klick auf den Button schließt
});
return header;
}
function autoInit() {
var headers = document.querySelectorAll('.ggs-header:not([data-ggs-enhanced])');
for (var i = 0; i < headers.length; i++) {
if (headers[i].getAttribute('data-ggs-no-float') === '1') continue;
try { enhance(headers[i]); }
catch (err) { if (window.console) console.warn('[ggs-header] enhance fehlgeschlagen:', err); }
}
}
window.GGSHeader = { enhance: enhance, autoInit: autoInit, icon: icon };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInit);
} else {
autoInit();
}
})();
+3 -1
View File
@@ -7,6 +7,8 @@
<link rel="icon" type="image/png" href="../../favicon-96x96.png" sizes="96x96">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ===== Energiemanager — Stufe 1 (2 Seen + 2 Pumpen + step-by-step) ===== */
@@ -741,7 +743,7 @@
<div class="em-layout">
<header class="ggs-header">
<header class="ggs-header" data-ggs-sim="energiemanager">
<a href="/schueler" class="ggs-header-logo" aria-label="GeoGraSim — zum Cockpit">
<img src="../../assets/img/bildLogo.png" alt="GeoGraSim" class="ggs-header-logo-icon">
</a>
+3 -1
View File
@@ -7,6 +7,8 @@
<link rel="icon" type="image/svg+xml" href="../../favicon.svg">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ==========================================================
FLUSSMANAGEMENT — modul-spezifisches CSS
@@ -112,7 +114,7 @@
<!-- ============================================================
HEADER
============================================================ -->
<header class="ggs-header">
<header class="ggs-header" data-ggs-sim="fluss">
<a href="/schueler" class="ggs-header-logo">
<img src="../../assets/img/bildLogo.png" alt="" class="ggs-header-logo-icon">
<img src="../../assets/img/textlogo_geograsim.svg" alt="GeoGraSim" class="ggs-header-logo-text">
+3 -1
View File
@@ -7,6 +7,8 @@
<link rel="icon" type="image/svg+xml" href="../../favicon.svg">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ================== MODUL-SPEZIFISCHE STYLES ================== */
@@ -666,7 +668,7 @@
<!-- ============================================================
HEADER
============================================================ -->
<header class="ggs-header" id="header">
<header class="ggs-header" id="header" data-ggs-sim="klima">
<a href="/schueler" class="ggs-header-logo">
<img src="../../assets/img/bildLogo.png" alt="" class="ggs-header-logo-icon">
<img src="../../assets/img/textlogo_geograsim.svg" alt="GeoGraSim" class="ggs-header-logo-text">
+3 -1
View File
@@ -7,6 +7,8 @@
<link rel="icon" type="image/svg+xml" href="../../favicon.svg">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ================== MODUL-SPEZIFISCHE STYLES ================== */
@@ -792,7 +794,7 @@
<!-- ============================================================
HEADER
============================================================ -->
<header class="ggs-header" id="header">
<header class="ggs-header" id="header" data-ggs-sim="klima">
<a href="/schueler" class="ggs-header-logo">
<img src="../../assets/img/bildLogo.png" alt="" class="ggs-header-logo-icon">
<img src="../../assets/img/textlogo_geograsim.svg" alt="GeoGraSim" class="ggs-header-logo-text">
+3 -1
View File
@@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Kofferdetektiv — GeoGraSim</title>
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ============ Design-System-Variablen (Fallback, identisch zur Plattform) ============ */
:root{
@@ -379,7 +381,7 @@ body.kd-touch #joystick,body.kd-touch #lookstick{display:block}
</div>
</div>
<header class="ggs-header">
<header class="ggs-header" data-ggs-sim="kofferdetektiv">
<a href="/schueler" class="ggs-header-logo" id="ggs-home-logo">
<img src="/assets/img/bildLogo.png" alt="" class="ggs-header-logo-icon" onerror="this.setAttribute('data-broken',1)">
<img src="/assets/img/textlogo_geograsim.svg" alt="GeoGraSim" class="ggs-header-logo-text" onerror="this.setAttribute('data-broken',1)">
+3 -1
View File
@@ -9,6 +9,8 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ===== Modul-spezifische Styles =====
Regel: Alles, was auch andere Module brauchen könnten, gehört ins
@@ -566,7 +568,7 @@ if (!window.LOGISTIK_BASE) {
<div class="lg-layout">
<!-- ============= Header (nach App/sims/template.html) ============= -->
<header class="ggs-header">
<header class="ggs-header" data-ggs-sim="logistik">
<a href="/schueler" class="ggs-header-logo">
<img src="../../assets/img/bildLogo.png" alt="" class="ggs-header-logo-icon">
<img src="../../assets/img/textlogo_geograsim.svg" alt="GeoGraSim" class="ggs-header-logo-text">