Files
demo-node-vs-js/index.html
T
2026-05-29 13:38:01 +02:00

1404 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JavaScript ≠ Browser · Web Engineering DHBW</title>
<meta name="description" content="Eine Sprache, zwei Laufzeiten: JS/ECMAScript im Browser vs. Node.js auf dem Server.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Bebas+Neue&family=Bungee&family=Bricolage+Grotesque:opsz,wght@12..96,300..800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
/* ============================================================
Design-Tokens — LibreTech-Markenwelt
============================================================ */
:root {
--bg: #14102b; /* Canvas */
--bg-2: #1b1540; /* Leiste / Panels */
--surface: #221953; /* Karten (= LibreTech "shadow") */
--surface-2: #2c2068;
--line: #3a2b7a; /* Konturen */
--line-soft: #2a1f5f;
--brand: #f6d939; /* Gelb (Face) */
--brand-2: #ea741e; /* Orange (Tiefe) */
--brand-3: #d63384; /* Pink (Akzent / aus den Folien) */
--text: #f3f0ff;
--dim: #c4bce6;
--faint:#8e84bf;
--shadow-col: #221953;
/* Welt-Kodierung (immer zusätzlich mit Text/Position kommuniziert) */
--c-browser: var(--brand);
--c-node: var(--brand-2);
--c-shared: var(--brand-3);
--r: 12px;
--maxw: 1180px;
--ease: cubic-bezier(.2,.7,.2,1);
--f-display: 'Anton', 'Arial Black', Impact, sans-serif;
--f-body: 'Bricolage Grotesque', system-ui, -apple-system, Segoe UI, sans-serif;
--f-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
/* ============================================================
Reset & Grundlayout
============================================================ */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: var(--f-body);
color: var(--text);
background:
radial-gradient(1200px 700px at 85% -10%, rgba(214,51,132,.18), transparent 60%),
radial-gradient(1100px 700px at -10% 110%, rgba(234,116,30,.15), transparent 55%),
var(--bg);
height: 100vh; height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
/* dezente Grid-Textur für Atmosphäre */
body::before {
content: "";
position: fixed; inset: 0;
background-image:
linear-gradient(rgba(255,255,255,.022) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.022) 1px, transparent 1px);
background-size: 44px 44px;
pointer-events: none;
z-index: 0;
}
a { color: var(--brand); text-underline-offset: 3px; }
.skip-link {
position: absolute; left: 8px; top: -60px;
background: var(--brand); color: #1a1230;
padding: 8px 14px; border-radius: 8px; font-weight: 700;
z-index: 100; transition: top .15s var(--ease);
}
.skip-link:focus { top: 8px; }
:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
border-radius: 4px;
}
/* ============================================================
Kopfleiste + Fortschritt
============================================================ */
.topbar {
position: relative; z-index: 5;
display: flex; align-items: center; gap: 16px;
padding: 10px clamp(14px, 3vw, 30px);
background: linear-gradient(180deg, rgba(27,21,64,.95), rgba(27,21,64,.78));
border-bottom: 1px solid var(--line);
backdrop-filter: blur(8px);
}
.wordmark {
font-family: var(--f-display);
font-size: 20px; letter-spacing: .02em;
color: var(--brand);
transform: skewX(-7deg);
text-shadow: 2px 2px 0 var(--brand-2), 3px 3px 0 var(--shadow-col);
white-space: nowrap; user-select: none;
}
.wordmark span { color: var(--text); }
.topbar .meta {
margin-left: auto; display: flex; align-items: center; gap: 14px;
font-family: var(--f-mono); font-size: 12px; color: var(--dim);
}
.screen-title {
font-family: var(--f-mono); font-size: 12px;
color: var(--text); text-transform: uppercase; letter-spacing: .14em;
}
.counter { color: var(--brand); font-weight: 600; }
.progress {
position: absolute; left: 0; bottom: -1px; height: 3px;
background: linear-gradient(90deg, var(--brand), var(--brand-3));
width: 0; transition: width .35s var(--ease);
}
/* ============================================================
Hauptbereich + Screens
============================================================ */
main {
position: relative; z-index: 1;
overflow-y: auto;
padding: clamp(18px, 3.4vw, 46px) clamp(14px, 4vw, 40px) 48px;
scroll-behavior: smooth;
}
.screen { max-width: var(--maxw); margin: 0 auto; }
.screen[hidden] { display: none; }
.eyebrow {
font-family: var(--f-mono); font-size: 12px;
letter-spacing: .22em; text-transform: uppercase;
color: var(--brand-3); margin: 0 0 10px;
}
h1, h2 { font-family: var(--f-display); font-weight: 400; line-height: 1.02; margin: 0; }
h2 {
font-size: clamp(28px, 4.6vw, 50px);
letter-spacing: .005em; color: var(--text);
}
h2 .y { color: var(--brand); }
.lead {
font-size: clamp(15px, 1.5vw, 18px); color: var(--dim);
max-width: 66ch; margin: 14px 0 0;
}
h3 {
font-family: var(--f-mono); font-weight: 600;
font-size: 13px; letter-spacing: .12em; text-transform: uppercase;
color: var(--brand); margin: 0 0 10px;
}
.screen :focus-visible { scroll-margin-top: 20px; }
h2[tabindex] { outline: none; }
/* gestaffelter Auftritt */
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
.is-entering > * { animation: rise .5s var(--ease) backwards; }
.is-entering > *:nth-child(1) { animation-delay: .02s; }
.is-entering > *:nth-child(2) { animation-delay: .08s; }
.is-entering > *:nth-child(3) { animation-delay: .14s; }
.is-entering > *:nth-child(4) { animation-delay: .20s; }
.is-entering > *:nth-child(5) { animation-delay: .26s; }
.card {
background: linear-gradient(180deg, var(--surface), var(--surface) 60%, var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r);
padding: clamp(16px, 2.4vw, 26px);
}
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.grid-aside { display: grid; grid-template-columns: 1.15fr .85fr; gap: 18px; align-items: start; }
@media (max-width: 860px) {
.grid-2, .grid-aside { grid-template-columns: 1fr; }
}
.tag {
display: inline-block; font-family: var(--f-mono); font-size: 11px;
padding: 3px 9px; border-radius: 999px; letter-spacing: .04em;
border: 1px solid var(--line);
}
.tag.browser { color: #1a1230; background: var(--c-browser); border-color: var(--c-browser); }
.tag.node { color: #1a1230; background: var(--c-node); border-color: var(--c-node); }
.tag.shared { color: #fff; background: var(--c-shared); border-color: var(--c-shared); }
/* ============================================================
Code-Darstellung
============================================================ */
pre, .code {
font-family: var(--f-mono); font-size: 13px; line-height: 1.6;
background: #0f0b22; color: #e9e5ff;
border: 1px solid var(--line-soft);
border-left: 3px solid var(--brand-3);
border-radius: 10px;
padding: 14px 16px; margin: 0;
overflow: auto; white-space: pre;
tab-size: 2;
}
.code { display: block; }
.tok-com { color: #7c74a8; font-style: italic; }
.tok-str { color: #f6d939; }
.tok-kw { color: #ff8fc4; }
.tok-num { color: #ffb56b; }
.note {
margin-top: 12px; padding: 12px 14px;
background: rgba(214,51,132,.10);
border: 1px solid rgba(214,51,132,.35);
border-radius: 10px; font-size: 14px; color: var(--text);
}
.note strong { color: var(--brand); }
/* Buttons */
.btn {
font-family: var(--f-mono); font-size: 13px;
padding: 9px 14px; border-radius: 9px; cursor: pointer;
background: var(--surface-2); color: var(--text);
border: 1px solid var(--line);
transition: transform .12s var(--ease), background .15s, border-color .15s;
}
.btn:hover { background: #372571; transform: translateY(-1px); border-color: var(--faint); }
.btn:active { transform: translateY(0); }
.btn.primary {
background: var(--brand); color: #1a1230; border-color: var(--brand); font-weight: 700;
}
.btn.primary:hover { background: #ffe65a; }
.btn.ghost { background: transparent; }
.btn-row { display: flex; flex-wrap: wrap; gap: 8px; }
/* ============================================================
Screen 0 — Hero
============================================================ */
.hero { min-height: 62vh; display: flex; flex-direction: column; justify-content: center; gap: 6px; }
.hero h1 {
font-size: clamp(36px, 6.5vw, 78px);
color: var(--text); line-height: 1.06;
margin: 12px 0 0; max-width: 16ch;
}
.hero h1 .y { color: var(--brand); }
.hero-vs {
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
font-family: var(--f-mono); font-size: 14px; color: var(--dim); margin-top: 10px;
}
.pill-world {
display: inline-flex; align-items: center; gap: 8px;
padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line);
background: var(--surface);
}
.pill-world.b { border-color: var(--c-browser); }
.pill-world.n { border-color: var(--c-node); }
.dot { width: 9px; height: 9px; border-radius: 50%; }
.dot.b { background: var(--c-browser); }
.dot.n { background: var(--c-node); }
.kbd-hint { font-family: var(--f-mono); font-size: 12.5px; color: var(--faint); margin-top: 26px; }
kbd {
font-family: var(--f-mono); font-size: 12px;
background: var(--surface-2); border: 1px solid var(--line);
border-bottom-width: 2px; border-radius: 6px; padding: 2px 7px; color: var(--text);
}
/* ============================================================
Screen 1 — Venn
============================================================ */
.venn-wrap { width: 100%; }
.venn { width: 100%; height: auto; display: block; }
.venn .circ { transition: fill-opacity .25s, stroke .2s; }
.venn .circ.on { stroke-width: 4; }
.venn text { font-family: var(--f-display); fill: var(--text); }
.venn .hint-api { font-family: var(--f-mono); font-size: 13px; fill: var(--faint); }
.chip-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
font-family: var(--f-mono); font-size: 12.5px; cursor: pointer;
padding: 7px 11px; border-radius: 8px; color: var(--text);
background: var(--surface); border: 1px solid var(--line);
display: inline-flex; align-items: center; gap: 8px;
transition: transform .12s var(--ease), background .15s;
}
.chip:hover { transform: translateY(-1px); }
.chip[aria-pressed="true"] { background: var(--surface-2); border-color: var(--faint); }
.chip .where {
font-size: 9.5px; text-transform: uppercase; letter-spacing: .06em;
padding: 2px 6px; border-radius: 999px; font-weight: 700;
}
.chip.browser .where { background: var(--c-browser); color: #1a1230; }
.chip.node .where { background: var(--c-node); color: #1a1230; }
.chip.shared .where { background: var(--c-shared); color: #fff; }
.chip[aria-pressed="true"].browser { border-color: var(--c-browser); }
.chip[aria-pressed="true"].node { border-color: var(--c-node); }
.chip[aria-pressed="true"].shared { border-color: var(--c-shared); }
#venn-note { min-height: 3.4em; }
/* ============================================================
Screen 2 — Zwei Konsolen
============================================================ */
.console-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--f-mono); font-size: 12px; letter-spacing: .08em;
text-transform: uppercase; margin-bottom: 8px;
}
.console-head .lamp { width: 10px; height: 10px; border-radius: 50%; }
.console.browser .lamp { background: var(--c-browser); }
.console.node .lamp { background: var(--c-node); }
.console pre {
min-height: 96px; border-left-color: var(--line-soft);
white-space: pre-wrap; word-break: break-word;
}
.console.browser pre { border-left-color: var(--c-browser); }
.console.node pre { border-left-color: var(--c-node); }
.prompt { color: var(--faint); }
.dim { color: var(--dim); }
.out-ok { color: #b7f5c2; }
.out-err { color: #ff9aa2; }
.ex-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.ex-btn {
font-family: var(--f-mono); font-size: 12.5px; cursor: pointer;
padding: 7px 11px; border-radius: 8px; color: var(--text);
background: #0f0b22; border: 1px solid var(--line);
transition: background .15s, border-color .15s, transform .12s var(--ease);
}
.ex-btn:hover { transform: translateY(-1px); border-color: var(--faint); }
.ex-btn[aria-pressed="true"] { border-color: var(--brand); color: var(--brand); }
/* ============================================================
Screen 3 — DOM Playground
============================================================ */
#dom-demo { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
#dom-demo li {
background: var(--surface-2); border: 1px solid var(--line);
border-radius: 9px; padding: 12px 14px;
font-family: var(--f-mono); font-size: 13.5px;
display: flex; align-items: center; gap: 10px;
transition: background .2s, border-color .2s, transform .2s var(--ease);
}
#dom-demo li .lbl::before { content: "<li>"; color: var(--faint); margin-right: 8px; }
#dom-demo li.hit { background: var(--brand); color: #1a1230; border-color: var(--brand); }
#dom-demo li.hit .lbl::before { color: #5a4a00; }
@keyframes pop { 0% { transform: scale(1); } 50% { transform: scale(1.03); } 100% { transform: scale(1); } }
.dom-echo { min-height: 64px; }
/* ============================================================
Tabs (Node / Syntax / Frameworks)
============================================================ */
.tablist {
display: inline-flex; flex-wrap: wrap; gap: 4px; margin-bottom: 18px;
background: #0f0b22; border: 1px solid var(--line);
border-radius: 12px; padding: 5px; max-width: 100%;
}
[role="tab"] {
font-family: var(--f-mono); font-size: 12.5px; cursor: pointer;
padding: 8px 14px; border-radius: 8px; color: var(--dim);
background: transparent; border: none;
transition: color .15s, background .15s;
}
[role="tab"]:hover { color: var(--text); background: rgba(255,255,255,.05); }
[role="tab"][aria-selected="true"] {
color: #1a1230; background: var(--brand); font-weight: 700;
}
[role="tabpanel"][hidden] { display: none; }
[data-run] { margin-top: 22px; }
.panel-note { font-family: var(--f-mono); font-size: 12px; color: var(--dim); margin: 0 0 10px; }
.fields { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.field-btn {
font-family: var(--f-mono); font-size: 12px; cursor: pointer;
padding: 6px 11px; border-radius: 999px; color: var(--brand);
background: rgba(246,217,57,.08); border: 1px solid rgba(246,217,57,.4);
transition: background .15s, transform .12s var(--ease);
}
.field-btn:hover { background: rgba(246,217,57,.18); transform: translateY(-1px); }
.field-btn[aria-pressed="true"] { background: var(--brand); color: #1a1230; }
#pkg-note { min-height: 3em; }
.run-out { min-height: 70px; margin-top: 12px; }
.kernidee {
font-family: var(--f-mono); font-size: 12.5px; color: var(--dim);
margin: 0 0 12px; padding: 8px 12px;
border-left: 3px solid var(--brand-3); background: rgba(255,255,255,.03); border-radius: 0 8px 8px 0;
}
/* ============================================================
Screen 6 — LibreTech Mini
============================================================ */
.lt-stage {
background: #362582; border-radius: var(--r); border: 1px solid var(--line);
min-height: 280px; display: flex; align-items: center; justify-content: center;
overflow: hidden; padding: 20px; transition: background .15s;
}
.lt-logo {
font-family: 'Anton', 'Bebas Neue', Impact, sans-serif;
font-size: clamp(48px, 9vw, 104px); color: #f6d939; line-height: 1.04;
letter-spacing: .01em; text-align: center; white-space: pre-line;
transform: rotate(-10deg) skewX(-8deg); user-select: none;
transition: color .15s;
}
.controls label { display: block; font-size: 12px; color: var(--dim); margin: 12px 0 4px; }
.controls .val { color: var(--brand); font-weight: 700; font-family: var(--f-mono); }
.controls input[type=range] { width: 100%; accent-color: var(--brand-3); }
.controls input[type=text],
.controls textarea,
.controls select {
width: 100%; padding: 7px 10px; font-family: var(--f-mono); font-size: 13px;
background: var(--bg); color: var(--text);
border: 1px solid var(--line); border-radius: 8px;
}
.controls textarea { resize: vertical; min-height: 52px; }
.controls .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.controls input[type=color] {
width: 100%; height: 34px; padding: 2px; border-radius: 8px;
border: 1px solid var(--line); background: var(--bg); cursor: pointer;
}
.uses-item { padding: 14px 0; border-top: 1px solid var(--line-soft); }
.uses-item:first-child { border-top: none; padding-top: 2px; }
.uses-code {
display: block; font-family: var(--f-mono); font-size: 12.5px; color: #e9e5ff;
background: #0f0b22; border: 1px solid var(--line-soft);
border-left: 3px solid var(--brand);
border-radius: 8px; padding: 9px 12px; overflow-x: auto; white-space: pre;
}
.uses-desc { margin: 9px 2px 0; font-size: 13.5px; color: var(--dim); line-height: 1.5; }
.uses-desc strong { color: var(--brand); font-weight: 600; }
.lt-css-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-bottom: 10px; flex-wrap: wrap;
}
.lt-css { color: #e9e5ff; white-space: pre; }
.copy-status { font-family: var(--f-mono); font-size: 12px; color: var(--brand); min-height: 1.2em; }
/* ============================================================
Fußleiste / Navigation
============================================================ */
.navbar {
position: relative; z-index: 5;
display: flex; align-items: center; gap: 14px;
padding: 10px clamp(14px, 3vw, 30px);
background: linear-gradient(0deg, rgba(27,21,64,.96), rgba(27,21,64,.78));
border-top: 1px solid var(--line);
backdrop-filter: blur(8px);
}
.dots { display: flex; gap: 8px; margin: 0 auto; }
.dots button {
width: 11px; height: 11px; padding: 0; border-radius: 50%; cursor: pointer;
background: var(--surface-2); border: 1px solid var(--line);
transition: background .2s, transform .15s var(--ease), width .2s;
}
.dots button:hover { transform: scale(1.25); }
.dots button[aria-current="true"] { background: var(--brand); border-color: var(--brand); width: 26px; border-radius: 6px; }
.nav-btn {
font-family: var(--f-mono); font-size: 13px; cursor: pointer;
display: inline-flex; align-items: center; gap: 8px;
padding: 9px 15px; border-radius: 9px; color: var(--text);
background: var(--surface); border: 1px solid var(--line);
transition: background .15s, transform .12s var(--ease), opacity .15s;
}
.nav-btn:hover:not(:disabled) { background: var(--surface-2); transform: translateY(-1px); }
.nav-btn:disabled { opacity: .35; cursor: not-allowed; }
.nav-btn.next { background: var(--brand); color: #1a1230; font-weight: 700; border-color: var(--brand); }
.nav-btn.next:hover:not(:disabled) { background: #ffe65a; }
.visually-hidden {
position: absolute; width: 1px; height: 1px; margin: -1px;
padding: 0; border: 0; clip: rect(0 0 0 0); overflow: hidden; white-space: nowrap;
}
/* ============================================================
Reduzierte Bewegung
============================================================ */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}
</style>
</head>
<body>
<a class="skip-link" href="#screen-host">Zum Inhalt springen</a>
<!-- Kopfleiste -->
<header class="topbar">
<div class="wordmark" aria-hidden="true">LIBRE<span>TECH</span></div>
<div class="meta">
<span class="screen-title" id="screen-title">Einführung</span>
<span class="counter"><span id="counter-cur">01</span> / 08</span>
</div>
<div class="progress" id="progress" aria-hidden="true"></div>
</header>
<!-- Hauptbereich -->
<main id="screen-host">
<!-- ===================== SCREEN 0 ===================== -->
<section class="screen hero" id="screen-0" data-title="Einführung" aria-labelledby="h-0">
<p class="eyebrow">Web Engineering · DHBW Stuttgart</p>
<h1 id="h-0">JavaScript im <span class="y">Browser</span> und in <span class="y">Node.js</span></h1>
<p class="lead">JavaScript ist eine Programmiersprache, definiert durch den ECMAScript-Standard. Ausgeführt wird sie von einer Laufzeitumgebung: im Browser oder durch Node.js auf dem Server. Beide verwenden dieselbe Sprach-Engine, stellen aber unterschiedliche Schnittstellen bereit — der Browser das DOM, Node.js den Zugriff auf Dateisystem, Netzwerk und Prozess. Diese Sitzung grenzt ab, was zur Sprache selbst gehört und was zur jeweiligen Umgebung.</p>
<div class="hero-vs" aria-hidden="true">
<span class="pill-world b"><span class="dot b"></span> Browser</span>
<span class="pill-world n"><span class="dot n"></span> Node.js</span>
<span class="pill-world"><span class="dot" style="background:var(--brand-3)"></span> gemeinsam: ECMAScript</span>
</div>
<p class="kbd-hint">Navigation: <kbd>←</kbd> <kbd>→</kbd> · <kbd>Leertaste</kbd> weiter · <kbd>1</kbd>–<kbd>8</kbd> direkt · <kbd>Pos 1</kbd>/<kbd>Ende</kbd></p>
</section>
<!-- ===================== SCREEN 1 ===================== -->
<section class="screen" id="screen-1" data-title="Modell" aria-labelledby="h-1" hidden>
<p class="eyebrow">Sprache und Umgebung</p>
<h2 id="h-1" tabindex="-1">ECMAScript-Kern und <span class="y">umgebungsspezifische APIs</span></h2>
<p class="lead">Eine Laufzeitumgebung besteht aus der Sprach-Engine (etwa V8) und den von ihr bereitgestellten Schnittstellen. Der ECMAScript-Kern ist in Browser und Node.js identisch; die übrigen APIs unterscheiden sich. Die folgende Übersicht ordnet einzelne APIs der jeweiligen Umgebung zu.</p>
<div class="grid-aside" style="margin-top:20px;">
<div class="card venn-wrap">
<svg class="venn" viewBox="0 0 640 360" role="img" aria-label="Venn-Diagramm: Browser und Node.js teilen sich den ECMAScript-Sprachkern in der Mitte.">
<circle id="region-browser" class="circ" cx="250" cy="185" r="155" fill="#f6d939" fill-opacity="0.12" stroke="#f6d939" stroke-width="2"></circle>
<circle id="region-node" class="circ" cx="390" cy="185" r="155" fill="#ea741e" fill-opacity="0.12" stroke="#ea741e" stroke-width="2"></circle>
<ellipse id="region-shared" class="circ" cx="320" cy="185" rx="74" ry="128" fill="#d63384" fill-opacity="0.12" stroke="#d63384" stroke-width="2"></ellipse>
<text x="168" y="62" font-size="26" text-anchor="middle">Browser</text>
<text x="472" y="62" font-size="26" text-anchor="middle">Node.js</text>
<text x="320" y="150" font-size="19" text-anchor="middle" fill="#d63384">geteilt</text>
<text class="hint-api" x="150" y="210" text-anchor="middle">document</text>
<text class="hint-api" x="150" y="232" text-anchor="middle">window</text>
<text class="hint-api" x="490" y="210" text-anchor="middle">node:fs</text>
<text class="hint-api" x="490" y="232" text-anchor="middle">node:http</text>
<text class="hint-api" x="320" y="190" text-anchor="middle">const · fetch</text>
<text class="hint-api" x="320" y="210" text-anchor="middle">Promise</text>
</svg>
</div>
<div>
<h3>APIs einordnen</h3>
<div class="chip-grid" id="chip-grid" role="group" aria-label="APIs auswählen, um die Zuordnung anzuzeigen"></div>
<div class="note" id="venn-note" role="status" aria-live="polite">Eine API auswählen, um zu sehen, in welcher Umgebung sie verfügbar ist.</div>
</div>
</div>
</section>
<!-- ===================== SCREEN 2 ===================== -->
<section class="screen" id="screen-2" data-title="Konsole" aria-labelledby="h-2" hidden>
<p class="eyebrow">Browser-Konsole und Node-REPL</p>
<h2 id="h-2" tabindex="-1">Denselben Ausdruck in <span class="y">beiden Umgebungen</span> auswerten</h2>
<p class="lead">Links wird der Ausdruck direkt in dieser Seite ausgewertet, also im Browser. Rechts steht die Ausgabe, die das Node-REPL liefert. Der Vergleich zeigt, welche Objekte in welcher Umgebung definiert sind.</p>
<div class="ex-row" id="ex-row" role="group" aria-label="Ausdruck auswählen" style="margin-top:18px;"></div>
<div class="grid-2">
<div class="console browser card">
<div class="console-head"><span class="lamp"></span> Browser-Konsole</div>
<pre id="out-browser" aria-live="polite" aria-label="Browser-Ausgabe"><span class="prompt">&gt;</span> <span class="dim">Ausdruck wählen …</span></pre>
</div>
<div class="console node card">
<div class="console-head"><span class="lamp"></span> Node REPL <span style="color:var(--faint)">($ node)</span></div>
<pre id="out-node" aria-live="polite" aria-label="Node-Ausgabe"><span class="prompt">&gt;</span> <span class="dim">Ausdruck wählen …</span></pre>
</div>
</div>
<div class="note"><strong>Beobachtung:</strong> Der Browser stellt DOM- und Fensterobjekte wie <code>document</code> und <code>window</code> bereit, Node den Zugriff auf Prozess und Dateisystem. Was eine Umgebung nicht definiert, führt dort zu einem <code>ReferenceError</code>.</div>
</section>
<!-- ===================== SCREEN 3 ===================== -->
<section class="screen" id="screen-3" data-title="DOM" aria-labelledby="h-3" hidden>
<p class="eyebrow">Browser-Umgebung</p>
<h2 id="h-3" tabindex="-1">Das <span class="y">Document Object Model</span></h2>
<p class="lead">Das Objekt <code>document</code> ist der Einstiegspunkt in das DOM — die Objektrepräsentation der geladenen Seite. Die folgenden Operationen verändern die Liste direkt; rechts steht der jeweils ausgeführte Aufruf. In Node existiert <code>document</code> nicht.</p>
<div class="grid-2" style="margin-top:18px;">
<div class="card">
<h3>Live-Liste</h3>
<ul id="dom-demo" aria-label="Demo-Liste zum Manipulieren">
<li><span class="lbl">Komponente A</span></li>
<li><span class="lbl">Komponente B</span></li>
<li><span class="lbl">Komponente C</span></li>
<li><span class="lbl">Komponente D</span></li>
</ul>
<div class="btn-row" style="margin-top:14px;">
<button class="btn" data-dom="count">querySelectorAll · zählen</button>
<button class="btn" data-dom="highlight">Erstes hervorheben</button>
<button class="btn" data-dom="text">Letzten Text ändern</button>
<button class="btn" data-dom="remove">Letztes entfernen</button>
<button class="btn ghost" data-dom="reset">Zurücksetzen</button>
</div>
</div>
<div class="card">
<h3>Konsolen-Echo</h3>
<pre class="dom-echo" id="dom-echo" aria-live="polite"><span class="prompt">&gt;</span> <span class="dim">Befehl ausführen …</span></pre>
<div class="note">In Node ist <code>document</code> nicht definiert; dieselben Aufrufe würden dort mit <code>ReferenceError: document is not defined</code> abbrechen.</div>
</div>
</div>
</section>
<!-- ===================== SCREEN 4 ===================== -->
<section class="screen" id="screen-4" data-title="Node.js" aria-labelledby="h-4" hidden>
<p class="eyebrow">Serverseitiges JavaScript</p>
<h2 id="h-4" tabindex="-1">Node.js: <span class="y">Dateisystem, Server und Prozess</span></h2>
<p class="lead">Node.js führt JavaScript außerhalb des Browsers aus — mit derselben V8-Engine, aber serverseitigen Modulen. <code>npm init</code> erzeugt die <code>package.json</code>; darunter folgen die wichtigsten eingebauten Module im ESM-Format mit <code>node:</code>-Präfix.</p>
<div class="card" style="margin-top:18px;">
<div class="tablist" role="tablist" aria-label="Node.js-Themen">
<button role="tab" id="nt-1" aria-controls="np-1" aria-selected="true" tabindex="0">package.json</button>
<button role="tab" id="nt-2" aria-controls="np-2" aria-selected="false" tabindex="-1">node:http</button>
<button role="tab" id="nt-3" aria-controls="np-3" aria-selected="false" tabindex="-1">node:fs</button>
<button role="tab" id="nt-4" aria-controls="np-4" aria-selected="false" tabindex="-1">process</button>
</div>
<div role="tabpanel" id="np-1" aria-labelledby="nt-1" tabindex="0">
<p class="panel-note">$ npm init -y &nbsp;erzeugt die <code>package.json</code>. Ein Feld auswählen für die Erläuterung:</p>
<div class="grid-aside">
<pre class="code" data-lang="json">{
"name": "mein-projekt",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js",
"test": "node --test"
},
"dependencies": {
"express": "^4.18.0"
}
}</pre>
<div>
<div class="fields" id="pkg-fields" role="group" aria-label="package.json-Felder erklären">
<button class="field-btn" data-pkg="type">type</button>
<button class="field-btn" data-pkg="main">main</button>
<button class="field-btn" data-pkg="scripts">scripts</button>
<button class="field-btn" data-pkg="dependencies">dependencies</button>
<button class="field-btn" data-pkg="version">version</button>
</div>
<div class="note" id="pkg-note" role="status" aria-live="polite">Wähle ein Feld für die Erklärung.</div>
</div>
</div>
</div>
<div role="tabpanel" id="np-2" aria-labelledby="nt-2" tabindex="0" hidden>
<p class="kernidee">Ein HTTP-Server kann nur serverseitig laufen; ein Browser kann selbst keinen Server bereitstellen.</p>
<pre class="code" data-lang="js">import { createServer } from "node:http";
const server = createServer((req, res) =&gt; {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("&lt;h1&gt;Hallo Welt!&lt;/h1&gt;");
});
server.listen(3000, () =&gt; {
console.log("Server läuft auf Port 3000");
});</pre>
</div>
<div role="tabpanel" id="np-3" aria-labelledby="nt-3" tabindex="0" hidden>
<p class="kernidee">Direkter Dateizugriff ist im Browser aus Sicherheitsgründen nicht möglich, in Node hingegen Standard.</p>
<pre class="code" data-lang="js">import { readFile } from "node:fs/promises";
// Promise-API (modern, empfohlen)
const data = await readFile("data.json", "utf8");
console.log(JSON.parse(data));</pre>
</div>
<div role="tabpanel" id="np-4" aria-labelledby="nt-4" tabindex="0" hidden>
<p class="kernidee">Das <code>process</code>-Objekt existiert nur in Node und steuert u. a. Signale, Argumente, Umgebungsvariablen und Exit-Codes.</p>
<pre class="code" data-lang="js">process.on("SIGINT", () =&gt; {
console.log("Received SIGINT. Closing server...");
server.close(() =&gt; process.exit(0));
});
process.exit(0); // 0 = Erfolg, 1 = Fehler</pre>
</div>
</div>
</section>
<!-- ===================== SCREEN 5 ===================== -->
<section class="screen" id="screen-5" data-title="ECMAScript" aria-labelledby="h-5" hidden>
<p class="eyebrow">Der ECMAScript-Sprachkern</p>
<h2 id="h-5" tabindex="-1">Moderne <span class="y">JavaScript-Syntax</span> (ES2015+)</h2>
<p class="lead">Diese Sprachmerkmale gehören zu ECMAScript und verhalten sich in Browser und Node identisch. Durchgängig wird modernes ES verwendet, ohne <code>var</code>. Ein Thema auswählen und den Code ausführen.</p>
<div class="card" style="margin-top:18px;">
<div class="tablist" role="tablist" aria-label="Syntax-Themen">
<button role="tab" id="st-1" aria-controls="sp-1" aria-selected="true" tabindex="0">const / let</button>
<button role="tab" id="st-fn" aria-controls="sp-fn" aria-selected="false" tabindex="-1">Funktionen</button>
<button role="tab" id="st-2" aria-controls="sp-2" aria-selected="false" tabindex="-1">Arrow Functions</button>
<button role="tab" id="st-3" aria-controls="sp-3" aria-selected="false" tabindex="-1">Destructuring</button>
<button role="tab" id="st-4" aria-controls="sp-4" aria-selected="false" tabindex="-1">Template Strings</button>
<button role="tab" id="st-5" aria-controls="sp-5" aria-selected="false" tabindex="-1">async / await</button>
<button role="tab" id="st-6" aria-controls="sp-6" aria-selected="false" tabindex="-1">IIFE</button>
</div>
<div role="tabpanel" id="sp-1" aria-labelledby="st-1" tabindex="0">
<pre class="code" data-lang="js">const TAX = 0.19; // konstant
let warenkorb = 100;
warenkorb += 50; // ✓ let ist veränderbar
// TAX = 0.07; // ✗ TypeError — const
{
let rabatt = 10; // lebt nur in diesem Block
}
// rabatt — außerhalb: ReferenceError</pre>
<button class="btn primary" data-run="constlet">▶ Ausführen</button>
<pre class="run-out" data-out="constlet" aria-live="polite"></pre>
</div>
<div role="tabpanel" id="sp-fn" aria-labelledby="st-fn" tabindex="0" hidden>
<pre class="code" data-lang="js">// Funktionsdeklaration mit Parametern, Rückgabewert
// und einem Standard-Parameter (satz)
function bruttoPreis(netto, satz = 0.19) {
return netto * (1 + satz);
}
bruttoPreis(100); // 119
bruttoPreis(100, 0.07); // 107 (satz überschrieben)</pre>
<button class="btn primary" data-run="funktionen">▶ Ausführen</button>
<pre class="run-out" data-out="funktionen" aria-live="polite"></pre>
</div>
<div role="tabpanel" id="sp-2" aria-labelledby="st-2" tabindex="0" hidden>
<pre class="code" data-lang="js">const preise = [19.99, 5, 12.5];
const summe = preise.reduce((a, b) =&gt; a + b, 0);
const teuer = preise.filter(p =&gt; p &gt; 10);
const inklMwSt = preise.map(p =&gt; p * 1.19);</pre>
<button class="btn primary" data-run="arrow">▶ Ausführen</button>
<pre class="run-out" data-out="arrow" aria-live="polite"></pre>
</div>
<div role="tabpanel" id="sp-3" aria-labelledby="st-3" tabindex="0" hidden>
<pre class="code" data-lang="js">const user = { name: "Mara", rolle: "Admin", aktiv: true };
const { name, rolle } = user; // herauslösen
const kopie = { ...user, aktiv: false }; // kopieren + ändern
const [erster, ...rest] = [1, 2, 3, 4]; // Rest-Elemente</pre>
<button class="btn primary" data-run="destructuring">▶ Ausführen</button>
<pre class="run-out" data-out="destructuring" aria-live="polite"></pre>
</div>
<div role="tabpanel" id="sp-4" aria-labelledby="st-4" tabindex="0" hidden>
<pre class="code" data-lang="js">const name = "Mara";
const punkte = 42;
const text = `Hallo ${name}, du hast ${punkte} Punkte.`;
// Ausdrücke direkt im String — z. B. für CSS:
const css = `transform: rotate(${punkte / 2}deg);`;</pre>
<button class="btn primary" data-run="template">▶ Ausführen</button>
<pre class="run-out" data-out="template" aria-live="polite"></pre>
</div>
<div role="tabpanel" id="sp-5" aria-labelledby="st-5" tabindex="0" hidden>
<pre class="code" data-lang="js">// "Daten laden" liefert ein Promise
const ladeUser = () =&gt;
new Promise(resolve =&gt;
setTimeout(() =&gt; resolve({ name: "Mara" }), 400)
);
const user = await ladeUser(); // wartet, ohne zu blockieren
console.log(user.name);</pre>
<button class="btn primary" data-run="asyncawait">▶ Ausführen</button>
<pre class="run-out" data-out="asyncawait" aria-live="polite"></pre>
</div>
<div role="tabpanel" id="sp-6" aria-labelledby="st-6" tabindex="0" hidden>
<pre class="code" data-lang="js">// IIFE: erzeugt einen eigenen, privaten Gültigkeitsbereich
const counter = (() =&gt; {
let n = 0; // von außen nicht zugänglich
return { tick: () =&gt; ++n }; // tick() ist die einzige Schnittstelle
})();
counter.tick(); // 1
counter.tick(); // 2</pre>
<button class="btn primary" data-run="iife">▶ Ausführen</button>
<pre class="run-out" data-out="iife" aria-live="polite"></pre>
</div>
</div>
</section>
<!-- ===================== SCREEN 6 ===================== -->
<section class="screen" id="screen-6" data-title="Beispiel" aria-labelledby="h-6" hidden>
<p class="eyebrow">Anwendungsbeispiel: Vanilla JavaScript</p>
<h2 id="h-6" tabindex="-1"><span class="y">Logo</span>-Generator</h2>
<p class="lead">Diese Anwendung kombiniert die behandelten Konzepte: DOM-Zugriff, Event-Handling und moderne Syntax — ohne Framework. Die Regler verändern die Vorschau; das erzeugte CSS unten aktualisiert sich entsprechend und lässt sich kopieren.</p>
<div class="grid-aside" style="margin-top:18px;">
<div class="lt-stage"><div class="lt-logo" id="lt-logo">LIBRE
TECH</div></div>
<div class="card controls">
<h3>Regler</h3>
<label for="lt-text">Text (mehrzeilig mit Enter)</label>
<textarea id="lt-text" rows="2">LIBRE
TECH</textarea>
<label for="lt-font">Schrift</label>
<select id="lt-font">
<option value="Anton">Anton</option>
<option value="Bungee">Bungee</option>
<option value="Bebas Neue">Bebas Neue</option>
<option value="Impact">Impact (System)</option>
</select>
<div class="row2">
<div>
<label for="lt-face">Face-Farbe</label>
<input type="color" id="lt-face" value="#f6d939">
</div>
<div>
<label for="lt-bg">Hintergrund</label>
<input type="color" id="lt-bg" value="#362582">
</div>
</div>
<div class="row2">
<div>
<label for="lt-depth">Tiefen-Farbe</label>
<input type="color" id="lt-depth" value="#ea741e">
</div>
<div>
<label for="lt-shadowcol">Schatten-Farbe</label>
<input type="color" id="lt-shadowcol" value="#221953">
</div>
</div>
<label for="lt-angle">Lichtwinkel: <span class="val" id="lt-angle-v">133</span>°</label>
<input type="range" id="lt-angle" min="0" max="359" value="133">
<label for="lt-shadow">Schatten-Länge: <span class="val" id="lt-shadow-v">22</span>px</label>
<input type="range" id="lt-shadow" min="0" max="60" value="22">
<label for="lt-depthlen">Tiefe: <span class="val" id="lt-depthlen-v">12</span>px</label>
<input type="range" id="lt-depthlen" min="0" max="30" value="12">
<label for="lt-contour">Kontur: <span class="val" id="lt-contour-v">7</span>px</label>
<input type="range" id="lt-contour" min="0" max="20" value="7">
<label for="lt-rot">Drehung: <span class="val" id="lt-rot-v">-10</span>°</label>
<input type="range" id="lt-rot" min="-25" max="25" value="-10">
</div>
</div>
<div class="card" style="margin-top:18px;">
<div class="lt-css-head">
<h3 style="margin:0;">Generiertes CSS</h3>
<button class="btn" id="lt-copy">CSS kopieren</button>
</div>
<pre class="lt-css" id="lt-css" aria-label="Generiertes CSS für das Logo"></pre>
<p class="copy-status" id="lt-copy-status" role="status" aria-live="polite"></p>
</div>
<div class="card" style="margin-top:18px;">
<h3>Verwendete Konzepte</h3>
<div class="uses-item">
<code class="uses-code">const $ = id =&gt; document.getElementById(id)</code>
<p class="uses-desc"><strong>Arrow Function</strong> und DOM-Zugriff: holt ein Element über seine ID.</p>
</div>
<div class="uses-item">
<code class="uses-code">$$("input, select, textarea").forEach(el =&gt; el.addEventListener("input", update))</code>
<p class="uses-desc"><strong>Event-Handling</strong>: bindet alle Regler gemeinsam; bei jeder Eingabe wird <code>update</code> aufgerufen.</p>
</div>
<div class="uses-item">
<code class="uses-code">await navigator.clipboard.writeText(css)</code>
<p class="uses-desc"><strong>Browser-API</strong> zum Kopieren in die Zwischenablage; in Node nicht verfügbar.</p>
</div>
</div>
</section>
<!-- ===================== SCREEN 7 ===================== -->
<section class="screen" id="screen-7" data-title="Ausblick" aria-labelledby="h-7" hidden>
<p class="eyebrow">Ausblick auf die nächste Sitzung</p>
<h2 id="h-7" tabindex="-1">Derselbe Zähler in <span class="y">vier Frameworks</span></h2>
<p class="lead">Dasselbe Verhalten — ein klickbarer Zähler — umgesetzt in vier Ansätzen, vom Web-Standard bis zum Compiler. In der nächsten Sitzung wird diese Logik mit einem Framework nachgebaut.</p>
<div class="card" style="margin-top:18px;">
<div class="tablist" role="tablist" aria-label="Framework-Beispiele">
<button role="tab" id="ft-1" aria-controls="fp-1" aria-selected="true" tabindex="0">Web Components</button>
<button role="tab" id="ft-2" aria-controls="fp-2" aria-selected="false" tabindex="-1">React</button>
<button role="tab" id="ft-3" aria-controls="fp-3" aria-selected="false" tabindex="-1">Vue</button>
<button role="tab" id="ft-4" aria-controls="fp-4" aria-selected="false" tabindex="-1">Svelte</button>
</div>
<div role="tabpanel" id="fp-1" aria-labelledby="ft-1" tabindex="0">
<p class="kernidee">Web-Standard, kein Framework — läuft nativ im Browser.</p>
<pre class="code" data-lang="js">class MyCounter extends HTMLElement {
constructor() {
super();
this.count = 0;
}
connectedCallback() {
this.innerHTML = `&lt;button&gt;Clicked 0 times&lt;/button&gt;`;
const btn = this.querySelector("button");
btn.addEventListener("click", () =&gt; {
this.count++;
btn.textContent = `Clicked ${this.count} times`;
});
}
}
customElements.define("my-counter", MyCounter);</pre>
</div>
<div role="tabpanel" id="fp-2" aria-labelledby="ft-2" tabindex="0" hidden>
<p class="kernidee">Virtual DOM · JSX · Hooks (useState).</p>
<pre class="code" data-lang="jsx">import { useState } from "react";
function MyButton() {
const [count, setCount] = useState(0);
return (
&lt;button onClick={() =&gt; setCount(count + 1)}&gt;
Clicked {count} times
&lt;/button&gt;
);
}</pre>
</div>
<div role="tabpanel" id="fp-3" aria-labelledby="ft-3" tabindex="0" hidden>
<p class="kernidee">Reaktive refs · Single-File Components (.vue).</p>
<pre class="code" data-lang="vue">&lt;script setup&gt;
import { ref } from "vue";
const count = ref(0);
&lt;/script&gt;
&lt;template&gt;
&lt;button @click="count++"&gt;
Clicked {{ count }} times
&lt;/button&gt;
&lt;/template&gt;</pre>
</div>
<div role="tabpanel" id="fp-4" aria-labelledby="ft-4" tabindex="0" hidden>
<p class="kernidee">Compiler — kein Runtime-Framework, reaktive Zuweisungen.</p>
<pre class="code" data-lang="svelte">&lt;script&gt;
let count = 0;
&lt;/script&gt;
&lt;button on:click={() =&gt; count += 1}&gt;
Clicked {count} {count === 1 ? 'time' : 'times'}
&lt;/button&gt;</pre>
</div>
</div>
<div class="note" style="margin-top:18px;">
<strong>Zusammenfassung:</strong>
Sprachkern (<span class="tag shared">ECMAScript</span>) läuft überall ·
<span class="tag browser">document/window</span> nur im Browser ·
<span class="tag node">node:fs · node:http · process</span> nur in Node. Frameworks bauen auf dem Browser-Teil auf.
</div>
</section>
</main>
<!-- Fußleiste / Navigation -->
<nav class="navbar" aria-label="Bildschirm-Navigation">
<button class="nav-btn prev" id="btn-prev">← Zurück</button>
<div class="dots" id="dots" role="group" aria-label="Direkt zu Bildschirm"></div>
<button class="nav-btn next" id="btn-next">Weiter →</button>
</nav>
<div class="visually-hidden" id="live-screen" role="status" aria-live="polite"></div>
<script>
/* IIFE — eigener Scope, sofort ausgeführt (genau das Pattern aus Screen 5). */
(() => {
"use strict";
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
/* ========================================================
Syntax-Highlighter (ein Durchlauf, sicher)
======================================================== */
const KEYWORDS = /\b(const|let|var|function|return|import|export|from|await|async|new|class|extends|super|if|else|for|of|in|try|catch|typeof|default|this)\b/;
function highlight(src) {
const esc = s => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const re = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(const|let|var|function|return|import|export|from|await|async|new|class|extends|super|if|else|for|of|in|try|catch|typeof|default|this)\b|\b(\d+(?:\.\d+)?)\b/g;
let out = "", last = 0, m;
while ((m = re.exec(src))) {
out += esc(src.slice(last, m.index));
if (m[1]) out += '<span class="tok-com">' + esc(m[1]) + "</span>";
else if (m[2]) out += '<span class="tok-str">' + esc(m[2]) + "</span>";
else if (m[3]) out += '<span class="tok-kw">' + esc(m[3]) + "</span>";
else if (m[4]) out += '<span class="tok-num">' + esc(m[4]) + "</span>";
last = re.lastIndex;
}
out += esc(src.slice(last));
return out;
}
$$(".code[data-lang]").forEach(el => { el.innerHTML = highlight(el.textContent); });
/* ========================================================
Screen-Navigation
======================================================== */
const screens = $$(".screen");
const total = screens.length;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let current = 0;
const elPrev = $("#btn-prev");
const elNext = $("#btn-next");
const elDots = $("#dots");
const elTitle = $("#screen-title");
const elCounter = $("#counter-cur");
const elProgress = $("#progress");
const elLive = $("#live-screen");
const pad2 = n => String(n).padStart(2, "0");
// Punkte erzeugen
screens.forEach((s, i) => {
const b = document.createElement("button");
b.type = "button";
b.setAttribute("aria-label", `Bildschirm ${i + 1}: ${s.dataset.title}`);
b.addEventListener("click", () => goTo(i));
elDots.appendChild(b);
});
const dots = $$("button", elDots);
function goTo(i) {
i = Math.max(0, Math.min(total - 1, i));
const target = screens[i];
screens.forEach((s, k) => {
if (k === i) {
s.hidden = false;
if (!reduceMotion) {
s.classList.add("is-entering");
s.addEventListener("animationend", function done() {
s.classList.remove("is-entering");
s.removeEventListener("animationend", done);
}, { once: true });
}
} else {
s.hidden = true;
s.classList.remove("is-entering");
}
});
current = i;
elTitle.textContent = target.dataset.title;
elCounter.textContent = pad2(i + 1);
elProgress.style.width = ((i + 1) / total * 100) + "%";
dots.forEach((d, k) => d.setAttribute("aria-current", k === i ? "true" : "false"));
elPrev.disabled = i === 0;
elNext.disabled = i === total - 1;
elLive.textContent = `Bildschirm ${i + 1} von ${total}: ${target.dataset.title}`;
// Fokus auf die Überschrift (außer ganz am Anfang)
const h = target.querySelector("h2[tabindex], h1");
if (h && h.hasAttribute("tabindex")) h.focus();
$("#screen-host").scrollTop = 0;
}
elPrev.addEventListener("click", () => goTo(current - 1));
elNext.addEventListener("click", () => goTo(current + 1));
document.addEventListener("keydown", e => {
const t = e.target;
const typing = t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT");
if (typing) return;
if (["ArrowRight", "PageDown", " "].includes(e.key)) { e.preventDefault(); goTo(current + 1); }
else if (["ArrowLeft", "PageUp"].includes(e.key)) { e.preventDefault(); goTo(current - 1); }
else if (e.key === "Home") { e.preventDefault(); goTo(0); }
else if (e.key === "End") { e.preventDefault(); goTo(total - 1); }
else if (/^[1-8]$/.test(e.key)) { e.preventDefault(); goTo(Number(e.key) - 1); }
});
/* ========================================================
Wiederverwendbare ARIA-Tabs (Pfeiltasten + roving tabindex)
======================================================== */
function initTabs(root) {
const tabs = $$('[role="tab"]', root);
const activate = (tab, focus = true) => {
tabs.forEach(t => {
const sel = t === tab;
t.setAttribute("aria-selected", String(sel));
t.tabIndex = sel ? 0 : -1;
const p = $("#" + t.getAttribute("aria-controls"), root);
if (p) p.hidden = !sel;
});
if (focus) tab.focus();
};
tabs.forEach((t, i) => {
t.addEventListener("click", () => activate(t, false));
t.addEventListener("keydown", e => {
let ni = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") ni = (i + 1) % tabs.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") ni = (i - 1 + tabs.length) % tabs.length;
else if (e.key === "Home") ni = 0;
else if (e.key === "End") ni = tabs.length - 1;
if (ni !== null) { e.preventDefault(); activate(tabs[ni]); }
});
});
}
$$('[role="tablist"]').forEach(list => initTabs(list.parentElement));
/* ========================================================
Screen 1 — Venn / API-Chips
======================================================== */
const apis = [
{ id: "document", where: "browser", note: "Einstieg ins DOM. Existiert nur im Browser — im Node-REPL: ReferenceError." },
{ id: "window", where: "browser", note: "Das globale Objekt im Browser. In Node heißt das Pendant globalThis." },
{ id: "navigator", where: "browser", note: "Infos zu Browser/Gerät (z. B. navigator.language). Kein Node-Äquivalent." },
{ id: "localStorage", where: "browser", note: "Key-Value-Speicher im Browser. In Node: Dateisystem oder Datenbank." },
{ id: "alert", where: "browser", note: "Blockierender Dialog — reine Browser-UI." },
{ id: "fetch", where: "shared", note: "Web-API aus dem Fetch-Standard. Im Browser seit Langem verfügbar, in Node seit v18 (über undici). Kein Sprachmerkmal, sondern eine Host-API." },
{ id: "console", where: "shared", note: "console.log & Co. bieten beide Runtimes — aber als Host-API, nicht als ECMAScript." },
{ id: "setTimeout", where: "shared", note: "Timer-API; von beiden Runtimes bereitgestellt, nicht Teil des Sprachkerns." },
{ id: "const / let", where: "shared", note: "ECMAScript-Sprachkern. Läuft in jeder konformen Engine — Browser wie Node." },
{ id: "Array.map", where: "shared", note: "Array-Methode aus dem Sprachkern. Überall verfügbar." },
{ id: "Promise", where: "shared", note: "ECMAScript-Sprachkern (ES2015). Basis für async/await." },
{ id: "async/await", where: "shared", note: "Sprachsyntax (ES2017). In beiden Umgebungen identisch." },
{ id: "node:fs", where: "node", note: "Dateisystem-Zugriff. Nur in Node — ein Browser darf das aus Sicherheitsgründen nicht." },
{ id: "node:http", where: "node", note: "Server starten & HTTP sprechen. Browser können keinen Server hosten." },
{ id: "process", where: "node", note: "Der laufende Node-Prozess (argv, env, platform, exit …) — ein Global, kein Modul. Im Browser nicht vorhanden." },
{ id: "node:path", where: "node", note: "Pfade plattformübergreifend bauen. Node-only." },
{ id: "__dirname", where: "node", note: "Verzeichnis der aktuellen Datei (CJS). In ESM via import.meta.url rekonstruieren." },
{ id: "require", where: "node", note: "CommonJS-Modulladen. Browser nutzen native ES-Module (import)." },
];
const whereLabel = { browser: "nur im Browser", node: "nur in Node.js", shared: "in beiden Umgebungen" };
const whereTag = { browser: "Browser", node: "Node", shared: "beide" };
const regionEl = { browser: $("#region-browser"), node: $("#region-node"), shared: $("#region-shared") };
const chipGrid = $("#chip-grid");
const vennNote = $("#venn-note");
apis.forEach(api => {
const b = document.createElement("button");
b.type = "button";
b.className = "chip " + api.where;
b.setAttribute("aria-pressed", "false");
b.innerHTML = `<span class="code-id">${api.id}</span> <span class="where">${whereTag[api.where]}</span>`;
b.addEventListener("click", () => {
$$(".chip", chipGrid).forEach(c => c.setAttribute("aria-pressed", "false"));
b.setAttribute("aria-pressed", "true");
Object.values(regionEl).forEach(r => { r.classList.remove("on"); r.setAttribute("fill-opacity", "0.12"); });
const r = regionEl[api.where];
r.classList.add("on");
r.setAttribute("fill-opacity", "0.4");
vennNote.innerHTML = `<strong>${api.id}</strong> — ${whereLabel[api.where]}. ${api.note}`;
});
chipGrid.appendChild(b);
});
/* ========================================================
Screen 2 — Zwei Konsolen
======================================================== */
const examples = [
{ expr: 'document.querySelector("h2").textContent', browser: () => document.querySelector("h2").textContent, node: "ReferenceError: document is not defined", nodeErr: true },
{ expr: "navigator.language", browser: () => navigator.language, node: "ReferenceError: navigator is not defined", nodeErr: true },
{ expr: "location.href", browser: () => location.href, node: "ReferenceError: location is not defined", nodeErr: true },
{ expr: "window.innerWidth", browser: () => window.innerWidth, node: "ReferenceError: window is not defined", nodeErr: true },
{ expr: "process.platform", browser: () => process.platform, node: "'linux'" },
{ expr: "process.cwd()", browser: () => process.cwd(), node: "'/home/mara/projekt'" },
];
const exRow = $("#ex-row");
const outBrowser = $("#out-browser");
const outNode = $("#out-node");
const fmt = v => {
if (typeof v === "string") return "'" + v + "'";
if (Array.isArray(v)) return "[ " + v.join(", ") + " ]";
return String(v);
};
const renderConsole = (el, expr, output, isErr) =>
el.innerHTML = `<span class="prompt">&gt;</span> ${expr}\n<span class="${isErr ? "out-err" : "out-ok"}">${output}</span>`;
examples.forEach((ex, i) => {
const b = document.createElement("button");
b.type = "button";
b.className = "ex-btn";
b.textContent = ex.expr;
b.setAttribute("aria-pressed", "false");
b.addEventListener("click", () => {
$$(".ex-btn", exRow).forEach(x => x.setAttribute("aria-pressed", "false"));
b.setAttribute("aria-pressed", "true");
let bOut, bErr = false;
try { bOut = fmt(ex.browser()); }
catch (err) { bOut = err.name + ": " + err.message; bErr = true; }
let nOut, nErr = !!ex.nodeErr;
if (typeof ex.node === "function") {
try { nOut = fmt(ex.node()); }
catch (err) { nOut = err.name + ": " + err.message; nErr = true; }
} else {
nOut = ex.node;
}
renderConsole(outBrowser, ex.expr, bOut, bErr);
renderConsole(outNode, ex.expr, nOut, nErr);
});
exRow.appendChild(b);
});
/* ========================================================
Screen 3 — DOM Playground
======================================================== */
const demo = $("#dom-demo");
const demoOriginal = demo.innerHTML;
const domEcho = $("#dom-echo");
const echo = (cmd, res) =>
domEcho.innerHTML = `<span class="prompt">&gt;</span> ${cmd}` + (res !== undefined ? `\n<span class="out-ok">${res}</span>` : "");
$$("[data-dom]").forEach(btn => {
btn.addEventListener("click", () => {
const action = btn.dataset.dom;
if (action === "count") {
const n = demo.querySelectorAll("li").length;
echo('document.querySelectorAll("#dom-demo li").length', n);
} else if (action === "highlight") {
const first = demo.querySelector("li");
if (first) {
first.classList.toggle("hit");
if (!reduceMotion) { first.style.animation = "pop .3s"; setTimeout(() => first.style.animation = "", 320); }
}
echo('document.querySelector("#dom-demo li").classList.toggle("hit")', first ? "✓" : "— leer");
} else if (action === "text") {
const last = demo.querySelector("li:last-child .lbl");
if (last) last.textContent = "Geändert";
echo('document.querySelector("#dom-demo li:last-child .lbl").textContent = "Geändert"', last ? "✓" : "— leer");
} else if (action === "remove") {
const last = demo.querySelector("li:last-child");
if (last) last.remove();
echo('document.querySelector("#dom-demo li:last-child")?.remove()', "verbleibend: " + demo.querySelectorAll("li").length);
} else if (action === "reset") {
demo.innerHTML = demoOriginal;
echo("// Liste auf den Ausgangszustand zurückgesetzt", demo.querySelectorAll("li").length + " Elemente");
}
});
});
/* ========================================================
Screen 4 — package.json-Felder
======================================================== */
const pkgNotes = {
type: "\"type\": \"module\" → ESM aktivieren. Dann gilt import/export statt require.",
main: "Einstiegspunkt des Pakets — die Datei, die beim Import geladen wird.",
scripts: "Benannte Kommandos: npm run dev, npm test … --watch startet bei Änderungen neu.",
dependencies: "Laufzeit-Abhängigkeiten. ^4.18.0 = kompatible Minor/Patch-Updates erlaubt.",
version: "Semantic Versioning: MAJOR.MINOR.PATCH.",
};
const pkgNote = $("#pkg-note");
$$("[data-pkg]").forEach(btn => {
btn.addEventListener("click", () => {
$$("[data-pkg]").forEach(b => b.setAttribute("aria-pressed", "false"));
btn.setAttribute("aria-pressed", "true");
pkgNote.innerHTML = `<strong>${btn.dataset.pkg}</strong> — ${pkgNotes[btn.dataset.pkg]}`;
});
});
/* ========================================================
Screen 5 — Syntax wirklich ausführen (kein eval)
======================================================== */
const runners = {
constlet() {
const TAX = 0.19;
let warenkorb = 100;
warenkorb += 50;
return [
"warenkorb → " + warenkorb + " (let: neu zugewiesen ✓)",
"TAX → " + TAX + " (const: gebunden)",
"inkl. MwSt → " + (warenkorb * (1 + TAX)).toFixed(2) + " €",
];
},
funktionen() {
function bruttoPreis(netto, satz = 0.19) {
return netto * (1 + satz);
}
return [
"bruttoPreis(100) → " + bruttoPreis(100).toFixed(2) + " €",
"bruttoPreis(100, 0.07) → " + bruttoPreis(100, 0.07).toFixed(2) + " €",
"Standard-Parameter satz = 0.19, falls nicht angegeben",
];
},
arrow() {
const preise = [19.99, 5, 12.5];
const summe = preise.reduce((a, b) => a + b, 0);
const teuer = preise.filter(p => p > 10);
const inklMwSt = preise.map(p => +(p * 1.19).toFixed(2));
return [
"summe → " + summe.toFixed(2) + " €",
"teuer → " + JSON.stringify(teuer),
"inklMwSt → " + JSON.stringify(inklMwSt),
];
},
destructuring() {
const user = { name: "Mara", rolle: "Admin", aktiv: true };
const { name, rolle } = user;
const kopie = { ...user, aktiv: false };
const [erster, ...rest] = [1, 2, 3, 4];
return [
"name → '" + name + "', rolle → '" + rolle + "'",
"kopie → " + JSON.stringify(kopie),
"erster → " + erster + ", rest → " + JSON.stringify(rest),
];
},
template() {
const name = "Mara", punkte = 42;
const text = `Hallo ${name}, du hast ${punkte} Punkte.`;
const css = `transform: rotate(${punkte / 2}deg);`;
return ["text → '" + text + "'", "css → '" + css + "'"];
},
async asyncawait() {
const ladeUser = () => new Promise(resolve => setTimeout(() => resolve({ name: "Mara" }), 400));
const user = await ladeUser();
return [
"await ladeUser() … 400 ms gewartet",
"user.name → '" + user.name + "'",
"→ nicht-blockierend: die Oberfläche bleibt flüssig",
];
},
iife() {
const counter = (() => { let n = 0; return { tick: () => ++n }; })();
const a = counter.tick(), b = counter.tick();
return [
"counter.tick() → " + a,
"counter.tick() → " + b,
"n bleibt verborgen — erreichbar nur über tick()",
];
},
};
$$("[data-run]").forEach(btn => {
btn.addEventListener("click", async () => {
const key = btn.dataset.run;
const out = $('[data-out="' + key + '"]');
out.innerHTML = `<span class="prompt">&gt;</span> <span class="dim">läuft …</span>`;
const result = runners[key]();
const lines = result instanceof Promise ? await result : result;
out.innerHTML = lines.map(l => `<span class="prompt">&gt;</span> <span class="out-ok">${l}</span>`).join("\n");
});
});
/* ========================================================
Screen 6 — Logo-Generator
======================================================== */
function buildShadow(angle, shadowLen, depthLen, contourLen, depthCol, shadowCol) {
const rad = angle * Math.PI / 180;
const dx = Math.sin(rad), dy = -Math.cos(rad);
const r = n => Math.round(n * 100) / 100;
const parts = [];
for (let i = 1; i <= contourLen; i++) parts.push(`${r(dx*i)}px ${r(dy*i)}px 0 ${shadowCol}`);
for (let i = 1; i <= depthLen; i++) parts.push(`${r(-dx*i)}px ${r(-dy*i)}px 0 ${depthCol}`);
for (let i = contourLen + 1; i <= contourLen + shadowLen; i++) parts.push(`${r(dx*i)}px ${r(dy*i)}px 0 ${shadowCol}`);
return parts.length ? parts.join(", ") : "none";
}
const ltLogo = $("#lt-logo");
const ltCss = $("#lt-css");
const ltCopy = $("#lt-copy");
const ltCopyStatus = $("#lt-copy-status");
const lt = {
text: $("#lt-text"), font: $("#lt-font"), face: $("#lt-face"), bg: $("#lt-bg"),
depth: $("#lt-depth"), shadowcol: $("#lt-shadowcol"),
angle: $("#lt-angle"), shadow: $("#lt-shadow"),
depthlen: $("#lt-depthlen"), contour: $("#lt-contour"), rot: $("#lt-rot"),
};
function ltUpdate() {
$("#lt-angle-v").textContent = lt.angle.value;
$("#lt-shadow-v").textContent = lt.shadow.value;
$("#lt-depthlen-v").textContent = lt.depthlen.value;
$("#lt-contour-v").textContent = lt.contour.value;
$("#lt-rot-v").textContent = lt.rot.value;
const shadow = buildShadow(
+lt.angle.value, +lt.shadow.value, +lt.depthlen.value,
+lt.contour.value, lt.depth.value, lt.shadowcol.value
);
ltLogo.textContent = lt.text.value || " ";
ltLogo.style.fontFamily = `'${lt.font.value}', 'Bebas Neue', Impact, sans-serif`;
ltLogo.style.color = lt.face.value;
ltLogo.style.transform = `rotate(${lt.rot.value}deg) skewX(-8deg)`;
ltLogo.style.textShadow = shadow;
$(".lt-stage").style.background = lt.bg.value;
const shadowOut = shadow === "none"
? " none"
: "\n " + shadow.split(", ").join(",\n ");
ltCss.textContent =
".logo {\n" +
` font-family: '${lt.font.value}', sans-serif;\n` +
` color: ${lt.face.value};\n` +
` transform: rotate(${lt.rot.value}deg) skewX(-8deg);\n` +
` text-shadow:${shadowOut};\n` +
"}";
}
Object.values(lt).forEach(el => {
el.addEventListener("input", ltUpdate);
el.addEventListener("change", ltUpdate);
});
ltCopy.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(ltCss.textContent);
ltCopyStatus.textContent = "In die Zwischenablage kopiert ✓";
} catch {
const range = document.createRange();
range.selectNodeContents(ltCss);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
ltCopyStatus.textContent = "Markiert — mit Strg/Cmd + C kopieren";
}
setTimeout(() => { ltCopyStatus.textContent = ""; }, 3000);
});
/* Start */
ltUpdate();
goTo(0);
})();
</script>
</body>
</html>