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:
1 parent
21e1d3009f
commit
1cce5ec492
3 files changed
+368
-16
No files matched your search
@@ -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
@@ -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 => ({ "&": "&", "<": "<", ">": ">" }[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(/</g, "<").replace(/>/g, ">").replace(/&/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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user