Slides lassen sich anlegen, kopieren, loeschen und umsortieren

Wie viele Slides ein Post hat, stand bisher im Code. SLIDES war ein festes
Array, der Stand speicherte nur Einstellungen dazu, und die durchgehende
Flaeche lag als 5400 px in der CSS.

Jetzt entsteht SLIDES aus einem Deck. Jeder Eintrag ist entweder eine
mitgelieferte Vorlage (ref) oder eine selbst angelegte Slide; eigene Texte
liegen als Ueberschreibung daneben. Fuer Vorlagen wandert damit nur ein
Verweis in den Stand, keine Kopie -- eine Aenderung an der Vorlage im Code
kommt weiterhin bei allen an, die sie nicht angefasst haben. Haette man die
Slides vollstaendig in den Stand geschrieben, waeren sie dort eingefroren.

Unter der Slide-Auswahl stehen vier Knoepfe: neu, kopieren, loeschen,
verschieben. Die Felder pro Slide (Verschiebungen, Muster, Layout) wandern
im Gleichschritt mit. Bleibt eine einzige Slide uebrig, ist der Post ein
Einzelbild: die Kopfzeile sagt es und der ZIP-Knopf verschwindet.

Neu ist die Gruppe "Inhalt (nur diese Slide)" mit Tag, Ueberschrift,
Fließtext und der Auswahl des Screenshots. Getippt wird Text, nicht
Auszeichnung: *Wort* setzt den Akzent, ein Zeilenumbruch bricht die Zeile um.
Gespeichert wird trotzdem fertiges Markup, weil die Vorlagen es so
mitbringen. Beim Bildwechsel folgt das Rahmenformat dem Seitenverhaeltnis
der Aufnahme, sonst stuende ein Hochformat in einem Querformat-Rahmen. Alle
Screens werden vorgeladen, nicht nur die gerade benutzten -- ein Bild, das
erst beim Auswaehlen geladen wuerde, fehlte im Export.

Der Stand steht auf v6. Ein Stand ohne Deck stammt aus der Zeit fester fuenf
Slides und behaelt die Vorlagen.

Dabei abgesichert: Der Tastaturschutz rief e.target.matches auf. Ist nichts
fokussiert, ist das Ziel das Dokument und hat kein matches. Seit der
Neuaufbau den zuletzt fokussierten Knopf entfernt, ist das erreichbar.

Geprueft mit 17 Faellen ueber das DevTools-Protokoll: anlegen, tippen,
Screenshot tauschen, kopieren, schieben, loeschen, Neustart. Dazu ein Export
einer selbst angelegten Slide und der volle Durchlauf der bestehenden elf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-07 17:36:54 +02:00
1 parent 21e1d3009f
commit 1cce5ec492
3 files changed
+368 -16

No files matched your search

+29 -1
View File
@@ -20,7 +20,35 @@ Downloads landen direkt in `~/Downloads`, ohne Browser-Dialog.
| Projekt | Datei | Inhalt |
|---|---|---|
| Post 1 · Lösung | `post1.html` | Carousel, 5 Slides |
| Post 1 · Lösung | `post1.html` | Carousel, Anzahl frei |
### Slides anlegen und wegnehmen
Wie viele Slides ein Post hat, steht nicht fest. Unter der Slide-Auswahl:
| Knopf | Wirkung |
|---|---|
| `+ Neu` | leere Slide dahinter einfügen |
| `Kopie` | die gewählte Slide samt Text, Bild und Verschiebungen verdoppeln |
| `Löschen` | die gewählte Slide wegnehmen (die letzte bleibt stehen) |
| `←` `→` | eine Position nach vorn oder hinten |
Bei einer einzigen Slide ist der Post ein **Einzelbild** — die Kopfzeile sagt
es, und der ZIP-Knopf verschwindet.
Unter **Inhalt (nur diese Slide)** stehen Tag, Überschrift, Fließtext und die
Auswahl des Screenshots. Getippt wird Text, keine Auszeichnung: `*Wort*` setzt
den Akzent, ein Zeilenumbruch bricht die Zeile um. Das Rahmenformat folgt beim
Wechsel automatisch dem Seitenverhältnis der gewählten Aufnahme.
> Die Tags sind gestalterisch ausgeblendet (`visibility: hidden` auf `.badge`).
> Der eingetippte Tag beschriftet deshalb vorerst nur den Slide-Knopf im
> Bedienfeld.
**Mitgelieferte Slides bleiben Vorlagen.** Der Stand speichert für sie nur
einen Verweis und die Felder, die du überschrieben hast — eine Änderung an der
Vorlage im Code kommt dadurch weiterhin bei allen an, die sie nicht angefasst
haben. Selbst angelegte Slides stehen vollständig im Stand.
**Das Projekt speichert seinen Stand** — Einstellungen, Layouts, Muster und
alle Verschiebungen aus dem Canvas überleben einen Reload. „Projekt
+333 -15
View File
@@ -33,6 +33,22 @@
height: 100vh; overflow: hidden; display: flex;
}
/* Inhaltsfelder der gewaehlten Slide. Eine Spalte, damit lange Texte nicht
in einer schmalen Zelle kleben. */
#inhalt { display: flex; flex-direction: column; gap: 4px; }
.inh-label { font-size: 11px; color: #8A8F9B; letter-spacing: .04em; margin-top: 4px; }
#inhalt input, #inhalt textarea, #inhalt select {
width: 100%; box-sizing: border-box;
background: #22242C; color: #D6DAE3; border: 1px solid #2E313B;
border-radius: 7px; padding: 7px 8px;
font-family: inherit; font-size: 12px; line-height: 1.45;
}
#inhalt textarea { resize: vertical; min-height: 46px; }
#inhalt input:focus, #inhalt textarea:focus, #inhalt select:focus {
outline: none; border-color: var(--accent-yellow);
}
#slide-ops button { flex: none; padding: 8px 10px; }
.saved {
font-size: 11px; color: #6FCF97; opacity: 0; transition: opacity .2s;
margin-left: 6px;
@@ -314,14 +330,14 @@
}
/* ─── Durchgehendes Carousel ──────────────────────────────────────────────
Der Post ist ein einziges 5400 x 1350 grosses Blatt, das in fünf 1080er
Fenster geschnitten wird. Jede Slide zeigt ihren Ausschnitt, indem die
Der Post ist ein einziges durchgehendes Blatt, das in 1080er Fenster
geschnitten wird. Seine Breite folgt der Anzahl der Slides (--sheet-w). Jede Slide zeigt ihren Ausschnitt, indem die
Ebene um index * 1080 nach links verschoben wird. Elemente auf den Nähten
werden dadurch zerschnitten und laufen auf der nächsten Slide weiter. */
.continuum {
position: absolute; top: 0; height: 100%;
left: calc(var(--slide-i, 0) * -1080px);
width: 5400px;
width: var(--sheet-w, 5400px);
pointer-events: none;
opacity: var(--int-mul, 1);
}
@@ -340,7 +356,7 @@
}
/* Ein Band über die volle Breite: die Klammer, die alle fünf zusammenhält. */
.ct-band {
left: 0; width: 5400px; height: 54px; bottom: 96px;
left: 0; width: var(--sheet-w, 5400px); height: 54px; bottom: 96px;
background: var(--shape-line);
}
.ct-g { color: var(--gmark-color); }
@@ -409,7 +425,7 @@
.sheet {
position: absolute; top: 0; height: 100%;
left: calc(var(--slide-i, 0) * -1080px);
width: 5400px; z-index: 1;
width: var(--sheet-w, 5400px); z-index: 1;
}
.page { position: absolute; top: 0; width: 1080px; height: 1350px; }
@@ -768,13 +784,25 @@
<aside class="panel">
<h1>Post 1 · Lösung</h1>
<p class="sub">Carousel, 5 Slides · 1080 × 1350 px<span class="saved" id="saved"></span><br><button class="forget" id="forget">Projekt zurücksetzen</button></p>
<p class="sub"><span id="kopfzeile">Carousel · 1080 × 1350 px</span><span class="saved" id="saved"></span><br><button class="forget" id="forget">Projekt zurücksetzen</button></p>
<div class="group">
<label>Slide</label>
<div class="opts one" id="slides-nav"></div>
<div class="opts" id="slide-ops" style="margin-top:8px">
<button id="slide-neu" title="Neue Slide dahinter einfügen">+ Neu</button>
<button id="slide-kopie" title="Diese Slide kopieren">Kopie</button>
<button id="slide-weg" title="Diese Slide löschen">Löschen</button>
<button id="slide-links" title="Eine Position nach vorn">←</button>
<button id="slide-rechts" title="Eine Position nach hinten">→</button>
</div>
</div>
<details class="group" data-grp="inhalt-nur-diese-slide">
<summary>Inhalt (nur diese Slide)</summary>
<div id="inhalt"></div>
</details>
<details class="group" data-grp="variante">
<summary>Variante</summary>
<div class="opts" id="variants">
@@ -964,7 +992,7 @@
Website say exactly the same thing; the headlines are new and short,
because a full sentence does not work as a carousel hook.
img-scale/x/y frame each capture on its telling region. */
const SLIDES = [
const VORLAGEN = [
{
key: "titel", type: "title",
layouts: [{ id: "klassisch", label: "Klassisch" }, { id: "wabe", label: "Wabe" }, { id: "laptop", label: "Laptop" }, { id: "laptop-logo", label: "Laptop · Logo" }, { id: "laptop-blur", label: "Laptop · Blur" }, { id: "laptop-footer", label: "Laptop · Logo unten" }, { id: "szene", label: "Laptop · Board" }],
@@ -1043,6 +1071,79 @@
},
];
/* Vorlage für eine selbst angelegte Slide. Sie startet bewusst mit Text
drin: eine leere Slide sieht aus wie ein Fehler, und man sieht nicht,
welches Feld welche Zeile füllt. */
const kopie = x => JSON.parse(JSON.stringify(x));
const NEUE_SLIDE = {
type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Neuer Tag",
head: 'Überschrift mit <span class="a">Akzent</span>',
body: "Beschreibung dieser Slide. Zwei bis drei Zeilen sitzen am besten.",
img: "./assets/screens/pipelines.png",
mockW: 1090, mockAR: 1.6,
imgScale: 1, imgX: "0%", imgY: "0%",
};
/* Alle Screens im Ordner. Eine selbst angelegte Slide kann jeden davon
wählen, deshalb werden sie vollständig geladen und nicht nur die, die
eine Vorlage zufällig gerade benutzt. */
const SCREENS = [
{ pfad: "./assets/screens/pipelines.png", name: "Pipelines · Board" },
{ pfad: "./assets/screens/pipelines-liste.png", name: "Pipelines · Liste" },
{ pfad: "./assets/screens/pipelines-detail.png", name: "Pipelines · Detail" },
{ pfad: "./assets/screens/workflow.png", name: "Workflow · Ablauf" },
{ pfad: "./assets/screens/workflow-bausteine.png", name: "Workflow · Bausteine" },
{ pfad: "./assets/screens/chat.png", name: "Chat" },
{ pfad: "./assets/screens/chat-tablet.png", name: "Chat · Tablet" },
{ pfad: "./assets/screens/chat-menue.png", name: "Chat · Menü" },
{ pfad: "./assets/screens/form-builder.png", name: "Form Builder" },
];
/* ─── Das Deck ────────────────────────────────────────────────────────────
Welche Slides der Post hat, steht nicht mehr fest. Jeder Eintrag ist
entweder eine mitgelieferte Vorlage (ref) oder eine selbst angelegte, und
eigene Texte liegen als Überschreibung daneben. So bleibt eine Änderung an
einer Vorlage im Code für alle wirksam, die sie nicht überschrieben haben
— hätte man die Slides vollständig in den Stand geschrieben, wäre der dort
eingefroren. */
let slideListe = VORLAGEN.map(v => ({ ref: v.key }));
const SLIDES = [];
function slideAus(eintrag, i) {
const basis = eintrag.ref ? VORLAGEN.find(v => v.key === eintrag.ref) : null;
const s = kopie(basis || NEUE_SLIDE);
Object.assign(s, eintrag.werte || {});
// Der Schlüssel landet im Dateinamen und muss eindeutig bleiben.
s.key = (basis ? basis.key : (eintrag.key || "eigen")) + (eintrag.ref ? "" : "-" + (i + 1));
return s;
}
function slidesNeuAufbauen() {
SLIDES.length = 0;
slideListe.forEach((e, i) => SLIDES.push(slideAus(e, i)));
}
/* Im Eingabefeld tippt man Text, nicht Auszeichnung: *Wort* wird zum Akzent,
ein Zeilenumbruch zum Umbruch. Gespeichert wird trotzdem das fertige
Markup, weil die Vorlagen es schon so mitbringen. */
function textZuHtml(t) {
return String(t)
.replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]))
.replace(/\*([^*\n]+)\*/g, '<span class="a">$1</span>')
.replace(/\r?\n/g, "<br>");
}
function htmlZuText(h) {
return String(h == null ? "" : h)
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<span class="a">([\s\S]*?)<\/span>/gi, "*$1*")
.replace(/<[^>]+>/g, "")
.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
/* Standardwerte. Reset und die Übersicht auf der Seite lesen beide von hier,
damit die Doku nicht auseinanderläuft. size = null heisst: nicht über die
Schriftgröße steuerbar (Bilder, Geräte). */
@@ -1085,6 +1186,8 @@
return Math.max(4, Math.round(stdSize(key, i) * 0.02));
}
slidesNeuAufbauen();
let edits = SLIDES.map(() => ({}));
let picked = null; // { slide, key }
@@ -1101,12 +1204,15 @@
const STORE = "hivegrid.projekt." + PROJECT;
const STATE_URL = "./state/" + PROJECT + ".json";
const STATE_V = 5;
const STATE_V = 6;
function currentState() {
return {
v: STATE_V, variant, tilt, superSample, continuous, view, tagOut, frameStyle,
current, layoutState, bgState, edits,
// Welche Slides der Post ueberhaupt hat. Ohne das waere eine selbst
// angelegte Slide nach dem Neuladen weg.
deck: slideListe,
};
}
@@ -1187,7 +1293,6 @@
// Tiefe Kopie: currentState() gibt die laufenden Arrays heraus, und beim
// Ziehen wird in edits[i][key] direkt hineingeschrieben. Ohne Kopie wuerde
// sich ein abgelegter Stand nachtraeglich mitveraendern.
const kopie = x => JSON.parse(JSON.stringify(x));
// Vergleich mit sortierten Schluesseln, damit die Reihenfolge, in der dx und
// dy angelegt wurden, keinen Unterschied vortaeuscht.
@@ -1234,9 +1339,17 @@
press("tagouts", "o", tagOut);
press("frames", "f", frameStyle);
press("views", "w", view);
SLIDES.forEach((s, i) => { if (s.type === "title") rebuildPage(i); });
applyAll();
applyEditsAll();
// Nach einem Schritt, der Slides hinzugefuegt, geloescht oder umsortiert
// hat, stimmt der Aufbau im Canvas nicht mehr mit dem Stand ueberein.
if (deck.querySelectorAll(".holder").length !== SLIDES.length) {
picked = null;
deckNeuBauen();
} else {
SLIDES.forEach((s, i) => rebuildPage(i));
applyAll();
applyEditsAll();
}
anzahlNachziehen();
show(current);
if (picked) pick(picked.slide, picked.key); else syncEditPanel();
} finally { restoring = false; }
@@ -1329,6 +1442,16 @@
eigene Funktion da: der Ladepfad und das Rueckgaengigmachen gehen beide
hier durch, sonst zieht der eine ein Feld nach, das der andere vergisst. */
function takeState(d) {
/* Zuerst das Deck: Anzahl und Art der Slides stehen darin, und alle
folgenden Felder haengen an dieser Anzahl. Ein Stand ohne Deck stammt
aus der Zeit fester fuenf Slides und behaelt die Vorlagen. */
if (Array.isArray(d.deck) && d.deck.length) {
slideListe = d.deck.map(e => ({
ref: e && e.ref, key: e && e.key, werte: (e && e.werte) || {},
}));
slidesNeuAufbauen();
passeFelderAn();
}
// Nur übernehmen, was zur aktuellen Slide-Anzahl passt: Änderungen am
// Aufbau dürfen keinen kaputten Stand einspielen.
if (Array.isArray(d.layoutState) && d.layoutState.length === SLIDES.length) {
@@ -1347,7 +1470,7 @@
view = d.view || view;
tagOut = d.tagOut || tagOut;
frameStyle = d.frameStyle || frameStyle;
current = Number.isInteger(d.current) ? d.current : 0;
current = Number.isInteger(d.current) ? Math.min(d.current, SLIDES.length - 1) : 0;
}
let savedTimer = null;
@@ -1767,6 +1890,10 @@
.concat(SLIDES.filter(s => s.back).map(s => s.back.img))
.concat(SLIDES.filter(s => s.media2).map(s => s.media2.img));
paths.push("./assets/screens/pipelines.png", "./assets/screens/chat-tablet.png");
/* Alle Screens, nicht nur die gerade benutzten: eine selbst angelegte
Slide kann jeden davon waehlen, und ein Bild, das erst beim Auswaehlen
geladen wuerde, fehlt im Export. */
SCREENS.forEach(sc => paths.push(sc.pfad));
// Optional: the layout falls back to a hint if the file is not there yet.
const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]);
paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png");
@@ -1838,6 +1965,7 @@
b.setAttribute("aria-pressed", String(n === current)));
syncBgButtons();
syncLayoutButtons();
syncInhalt();
fitStrip();
metaEl.textContent = view === "streifen"
? "Slide " + (current + 1) + " ausgewählt. Klick auf eine Slide wechselt die Auswahl"
@@ -2212,6 +2340,191 @@
setStatus("Fehler: " + err.message, "err");
});
/* ─── Slides verwalten ────────────────────────────────────────────────────
Die Felder pro Slide (Verschiebungen, Muster, Layout) haengen an der
Anzahl und muessen bei jeder Aenderung im Gleichschritt mitwandern. Das
laeuft ueber slideListe als Bezugsgroesse, nicht ueber SLIDES: SLIDES
wird daraus jedes Mal neu berechnet. */
function passeFelderAn() {
const n = SLIDES.length;
while (edits.length < n) edits.push({});
edits.length = n;
while (bgState.length < n) bgState.push({ bg: "formen", int: "dezent" });
bgState.length = n;
while (layoutState.length < n) layoutState.push(SLIDES[layoutState.length].layouts[0].id);
layoutState.length = n;
// Ein Layout, das es auf dieser Slide nicht gibt, faellt auf das erste
// zurueck: sonst zeigt eine Feature-Slide ein Titel-Layout.
layoutState.forEach((id, i) => {
if (!SLIDES[i].layouts.some(l => l.id === id)) layoutState[i] = SLIDES[i].layouts[0].id;
});
}
function deckNeuBauen() {
deck.innerHTML = "";
nav.innerHTML = "";
buildDeck();
applyAll();
applyEditsAll();
}
function anzahlNachziehen() {
const n = SLIDES.length;
const kopf = document.getElementById("kopfzeile");
if (kopf) kopf.textContent =
(n === 1 ? "Einzelbild" : "Carousel, " + n + " Slides") + " · 1080 × 1350 px";
const zip = document.getElementById("dl-all");
if (zip) {
zip.textContent = "Alle " + n + " Slides als ZIP";
// Bei einer einzigen Slide ist ein ZIP nur ein Umweg.
zip.style.display = n > 1 ? "" : "none";
}
// Das durchgehende Blatt ist so breit wie der Post lang ist.
deck.style.setProperty("--sheet-w", (n * 1080) + "px");
}
function nachDeckAenderung(an, meldung) {
slidesNeuAufbauen();
passeFelderAn();
picked = null;
deckNeuBauen();
current = Math.max(0, Math.min(an, SLIDES.length - 1));
show(current);
anzahlNachziehen();
syncInhalt();
syncEditPanel();
saveState();
if (meldung) setStatus(meldung, "ok");
}
function slideEinfuegen(eintrag, an) {
slideListe.splice(an, 0, eintrag);
edits.splice(an, 0, {});
bgState.splice(an, 0, kopie(bgState[current] || { bg: "formen", int: "dezent" }));
slidesNeuAufbauen();
layoutState.splice(an, 0, SLIDES[an].layouts[0].id);
}
function slideNeu() {
const an = current + 1;
slideEinfuegen({ werte: {} }, an);
nachDeckAenderung(an, "Slide " + (an + 1) + " angelegt");
}
function slideKopieren() {
const an = current + 1;
const q = slideListe[current];
slideEinfuegen({ ref: q.ref, key: q.key, werte: kopie(q.werte || {}) }, an);
// Auch die Feinarbeit mitnehmen, sonst ist die Kopie nur halb eine.
edits[an] = kopie(edits[current]);
bgState[an] = kopie(bgState[current]);
layoutState[an] = layoutState[current];
nachDeckAenderung(an, "Slide kopiert");
}
function slideLoeschen() {
if (SLIDES.length <= 1) { setStatus("Die letzte Slide bleibt stehen", "err"); return; }
const weg = current;
[slideListe, edits, bgState, layoutState].forEach(a => a.splice(weg, 1));
nachDeckAenderung(Math.min(weg, SLIDES.length - 2), "Slide gelöscht");
}
function slideSchieben(um) {
const ziel = current + um;
if (ziel < 0 || ziel >= SLIDES.length) return;
[slideListe, edits, bgState, layoutState].forEach(a => {
const [x] = a.splice(current, 1);
a.splice(ziel, 0, x);
});
nachDeckAenderung(ziel, "Slide verschoben");
}
/* ─── Inhalt der gewaehlten Slide ─────────────────────────────────────────
Geschrieben wird in slideListe[i].werte, nicht in SLIDES: SLIDES entsteht
aus der Vorlage plus diesen Ueberschreibungen. Wer nichts anfasst, bekommt
Aenderungen an der Vorlage im Code weiterhin mit. */
function werteVon(i) {
const e = slideListe[i];
return (e.werte = e.werte || {});
}
function syncInhalt() {
const box = document.getElementById("inhalt");
if (!box) return;
const s = SLIDES[current];
const id = current + ":" + (s.type || "");
if (box.dataset.for !== id) {
box.dataset.for = id;
const felder = s.type === "feature"
? [["eyebrow", "Tag", "input"], ["head", "Überschrift", "area"], ["body", "Fließtext", "area"]]
: [["head", "Überschrift", "area"], ["sub", "Claim", "area"]];
box.innerHTML = felder.map(([k, label, art]) =>
'<label class="inh-label" for="in-' + k + '">' + label + '</label>' +
(art === "area"
? '<textarea id="in-' + k + '" data-feld="' + k + '" rows="2"></textarea>'
: '<input id="in-' + k + '" data-feld="' + k + '" type="text">')
).join("") +
(s.type === "feature"
? '<label class="inh-label" for="in-img">Screenshot</label>' +
'<select id="in-img" data-feld="img">' +
SCREENS.map(sc => '<option value="' + sc.pfad + '">' + sc.name + '</option>').join("") +
'</select>'
: "") +
'<div class="hint">*Wort* setzt den Akzent, ein Zeilenumbruch bricht die Zeile um.' +
(s.type === "title" ? ' Beim Titel wirkt das nur im Layout „Klassisch“.' : '') + '</div>';
}
box.querySelectorAll("[data-feld]").forEach(el => {
if (document.activeElement === el) return;
const k = el.dataset.feld;
el.value = k === "img" ? (s.img || "") : htmlZuText(s[k]);
});
}
document.getElementById("inhalt").addEventListener("input", ev => {
const el = ev.target.closest("[data-feld]");
if (!el) return;
const k = el.dataset.feld;
const w = werteVon(current);
if (k === "img") {
w.img = el.value;
// Das Rahmenformat folgt dem Bild, sonst steht ein Hochformat in einem
// Querformat-Rahmen und laesst links und rechts Flaechen frei.
const im = new Image();
im.onload = () => {
w.mockAR = +(im.naturalWidth / im.naturalHeight).toFixed(4);
w.imgScale = 1; w.imgX = "0%"; w.imgY = "0%";
slidesNeuAufbauen();
// Das Rahmenformat steckt im Aufbau der Seite, nicht in ihren Texten:
// dafuer reicht rebuildPage nicht, der Halter muss neu entstehen.
deckNeuBauen();
show(current);
saveState();
};
im.src = assets[el.value] || el.value;
return;
}
w[k] = textZuHtml(el.value);
slidesNeuAufbauen();
// Nur die betroffene Seite neu zeichnen: ein voller Neuaufbau wuerde das
// Eingabefeld unter den Fingern wegreissen.
rebuildPage(current);
applyEditsAll();
nav.querySelectorAll("button")[current].innerHTML =
'<span class="num">' + (current + 1) + '</span>' +
(SLIDES[current].type === "title" ? "Titel" : SLIDES[current].eyebrow);
saveState();
});
document.getElementById("slide-ops").addEventListener("click", ev => {
const b = ev.target.closest("button");
if (!b || stateLocked) return;
if (b.id === "slide-neu") slideNeu();
else if (b.id === "slide-kopie") slideKopieren();
else if (b.id === "slide-weg") slideLoeschen();
else if (b.id === "slide-links") slideSchieben(-1);
else if (b.id === "slide-rechts") slideSchieben(1);
});
function buildDeck() {
SLIDES.forEach((sl, i) => {
const holder = document.createElement("div");
@@ -2788,7 +3101,10 @@
document.addEventListener("keydown", e => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
// In einem Eingabefeld gehoeren die Tasten dem Feld, nicht den Kuerzeln.
if (e.target.matches("input, textarea, select")) return;
// Ist nichts fokussiert, ist das Ziel das Dokument selbst — das kennt
// kein matches. Seit Slides hinzukommen und wegfallen koennen, ist genau
// das erreichbar: der Neuaufbau entfernt den zuletzt fokussierten Knopf.
if (e.target && e.target.matches && e.target.matches("input, textarea, select")) return;
const k = e.key.toLowerCase();
if (editMode === "an" && picked && e.key.startsWith("Arrow")) {
const step = e.shiftKey ? 10 : 1;
@@ -2852,12 +3168,14 @@
press("tagouts", "o", tagOut);
press("frames", "f", frameStyle);
press("views", "w", view);
anzahlNachziehen();
show(current);
if (restored) setStatus("Projektstand geladen", "ok");
await document.fonts.ready;
fontEmbedCss = await htmlToImage.getFontEmbedCSS(deck.querySelector(".slide"));
["dl-png", "dl-all"].forEach(id => document.getElementById(id).disabled = false);
if (checkCollisions()) setStatus("Bereit. 5 Slides, 1080 × 1350 px", "ok");
if (checkCollisions()) setStatus("Bereit. " + SLIDES.length +
(SLIDES.length === 1 ? " Slide" : " Slides") + ", 1080 × 1350 px", "ok");
} catch (err) {
console.error(err);
setStatus("Fehler beim Laden: " + err.message, "err");
+6
View File
@@ -207,6 +207,12 @@ schon zweimal zu viel.
### Formate
**Die Anzahl der Slides ist frei.** Sie kommt aus dem Deck im Projektstand,
nicht aus dem Code. Das durchgehende Blatt (§5, „Durchgehend") ist deshalb
`--sheet-w` breit statt fester 5400 px; die vier Sechsecke auf den Nähten
sitzen weiterhin auf 1080 / 2160 / 3240 / 4320 und decken damit die ersten
fünf Slides ab. Wer länger baut, bekommt dahinter nur noch das Band.
**1080 × 1350 px (4:5)** — das hochkantigste offiziell unterstützte
Feed-Format. Belegt: Instagram unterstützt 1.91:1 bis 4:5, Breiten 320–1080
behalten die Originalauflösung, alles außerhalb wird beschnitten.