diff --git a/post1.html b/post1.html
index 451c939..6cbd351 100644
--- a/post1.html
+++ b/post1.html
@@ -275,7 +275,6 @@
sein TEXT dadurch um die 20 px Innenabstand eingerückt gegenüber der
Headline. Das negative Margin holt den Text auf dieselbe Linie. Zum
Vergleichen umschaltbar. */
- .slide[data-tagout="an"] .badge { margin-left: -20px; }
.sh { position: absolute; }
@@ -427,18 +426,23 @@
/* Section badge, straight from the site: .chamfer-badge in globals.css with
accent-yellow-light on accent-yellow-dark. Scaled from the 12px web size to
- this canvas; the 5px corner cut scales with it. */
+ this canvas; the 5px corner cut scales with it.
+ Ausgeblendet statt entfernt: das Tag steht im Textfluss, ohne seine Box
+ ruecken Ueberschrift und Text nach oben. visibility haelt den Platz,
+ display: none wuerde ihn aufgeben. */
.badge {
display: inline-block; margin-bottom: 38px;
font-size: 26px; font-weight: 600; letter-spacing: 0.1em;
padding: 9px 20px;
background: var(--accent-yellow-light);
color: var(--accent-yellow-dark);
+ visibility: hidden;
clip-path: polygon(
11px 0, calc(100% - 11px) 0, 100% 11px, 100% calc(100% - 11px),
calc(100% - 11px) 100%, 11px 100%, 0 calc(100% - 11px), 0 11px
);
}
+ .slide[data-tagout="an"] .badge { margin-left: -20px; }
.headline {
font-size: 74px; font-weight: 800;
@@ -913,6 +917,7 @@
E Bearbeiten · Pfeile verschieben (⇧ = 10 px)
+ − 0 Zoom · ⌘/Strg + Rad
Leer + ziehen verschiebt die Fläche
+ ⌘/Strg+Z Rückgängig (⇧ = wiederherstellen)
D Slide laden · A alle als ZIP
@@ -976,24 +981,6 @@
head: 'Vom Handwerker
f\u00fcr Handwerker',
sub: "Die Software, die deine Branchenprogramme nicht ersetzt, sondern verbindet.",
},
- {
- key: "projektmanagement", type: "feature",
- layouts: [{ id: "standard", label: "Standard" }],
- eyebrow: "Vertrieb",
- head: 'Du siehst auf einen Blick,
wo alle Auftr\u00e4ge stehen.',
- body: "Projekte, Kunden, Angebote, F\u00f6rderantr\u00e4ge. Kein Suchen, kein Nachfragen, kein Zettelchaos.",
- img: "./assets/screens/pipelines.png",
- // Nur die linke Bildhälfte: Zoom 2 bei Versatz 0 zeigt exakt den Anteil
- // 0 bis 0,5. Das Rahmenverhältnis ist die halbierte Bildbreite.
- mockW: 580, mockAR: 0.8855,
- imgScale: 2, imgX: "0%", imgY: "0%",
- // Detailspalte eines Projekts vor dem Board: zeigt, was hinter einer
- // Karte steckt, ohne das Board zu verdecken.
- media2: {
- img: "./assets/screens/pipelines-detail.png",
- w: 300, ar: 0.781, left: 500, bottom: 52, z: 2, rot: -2,
- },
- },
{
key: "workflow-engine", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
@@ -1015,6 +1002,24 @@
w: 320, left: 44, bottom: 150, z: 3, rot: -3,
},
},
+ {
+ key: "projektmanagement", type: "feature",
+ layouts: [{ id: "standard", label: "Standard" }],
+ eyebrow: "Vertrieb",
+ head: 'Du siehst auf einen Blick,
wo alle Auftr\u00e4ge stehen.',
+ body: "Projekte, Kunden, Angebote, F\u00f6rderantr\u00e4ge. Kein Suchen, kein Nachfragen, kein Zettelchaos.",
+ img: "./assets/screens/pipelines.png",
+ // Nur die linke Bildhälfte: Zoom 2 bei Versatz 0 zeigt exakt den Anteil
+ // 0 bis 0,5. Das Rahmenverhältnis ist die halbierte Bildbreite.
+ mockW: 580, mockAR: 0.8855,
+ imgScale: 2, imgX: "0%", imgY: "0%",
+ // Detailspalte eines Projekts vor dem Board: zeigt, was hinter einer
+ // Karte steckt, ohne das Board zu verdecken.
+ media2: {
+ img: "./assets/screens/pipelines-detail.png",
+ w: 300, ar: 0.781, left: 500, bottom: 52, z: 2, rot: -2,
+ },
+ },
{
key: "chat", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
@@ -1106,7 +1111,7 @@
const STORE = "hivegrid.projekt." + PROJECT;
const STATE_URL = "./state/" + PROJECT + ".json";
- const STATE_V = 3;
+ const STATE_V = 5;
function currentState() {
return {
@@ -1141,6 +1146,23 @@
if (alt < 3 && Array.isArray(d.edits)) {
for (const e of d.edits) { if (e) delete e.szene; }
}
+ // Stand 4: Automation & KI steht jetzt vor Vertrieb. Der Stand haengt an der
+ // Position, nicht am Slide-Schluessel — ohne diesen Tausch laegen die
+ // gezogenen Positionen, das Muster und das Layout an der falschen Slide.
+ if (alt < 4) {
+ const tausch = arr => {
+ if (Array.isArray(arr) && arr.length > 2) { const t = arr[1]; arr[1] = arr[2]; arr[2] = t; }
+ };
+ tausch(d.edits); tausch(d.bgState); tausch(d.layoutState);
+ if (d.current === 1) d.current = 2;
+ else if (d.current === 2) d.current = 1;
+ }
+ // Stand 5: Das Tag ist ausgeblendet, seine gespeicherte Stelle also ohne
+ // Wirkung. Der Eintrag wird geraeumt, damit keine Werte stehenbleiben,
+ // die niemand mehr nachvollziehen kann.
+ if (alt < 5 && Array.isArray(d.edits)) {
+ for (const e of d.edits) { if (e) delete e.badge; }
+ }
d.v = STATE_V;
return true;
}
@@ -1164,9 +1186,96 @@
+ "damit die vorhandene Datei nicht ueberschrieben wird.", "err");
}
+ /* ─── Rueckgaengig ───────────────────────────────────────────────────────
+ Haengt an saveState statt an den 25 Stellen, die etwas aendern: was
+ gespeichert wird, ist genau das, was rueckgaengig gemacht werden soll.
+ Ziehen speichert erst beim Loslassen, eine Bewegung ist also bereits ein
+ Schritt und nicht hundert. */
+ const UNDO_MAX = 100;
+ let undoStack = [], redoStack = [], lastSnap = null, restoring = false;
+
+ // 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.
+ function stabil(x) {
+ if (Array.isArray(x)) return x.map(stabil);
+ if (x && typeof x === "object") {
+ const o = {};
+ for (const k of Object.keys(x).sort()) o[k] = stabil(x[k]);
+ return o;
+ }
+ return x;
+ }
+
+ // Ein Slide-Wechsel allein ist nichts, was man rueckgaengig machen will.
+ // show() speichert bei jedem Wechsel; ohne diese Ausnahme bestuende der
+ // Stapel zur Haelfte aus Auswahlwechseln.
+ function nurAuswahlAnders(a, b) {
+ const ohne = s => { const c = Object.assign({}, s); delete c.current; return JSON.stringify(stabil(c)); };
+ return ohne(a) === ohne(b);
+ }
+
+ function merkeFuerUndo(state) {
+ const snap = kopie(state);
+ if (lastSnap && !nurAuswahlAnders(lastSnap, snap)) {
+ undoStack.push(lastSnap);
+ if (undoStack.length > UNDO_MAX) undoStack.shift();
+ redoStack.length = 0; // neuer Zweig: das Wiederherstellen verfaellt
+ }
+ lastSnap = snap;
+ }
+
+ /* Zurueckspielen laeuft ueber takeState, also denselben Weg wie das Laden,
+ und zieht danach die gesamte Oberflaeche nach. Von Hand jeden Schalter
+ einzeln zu setzen waere die Stelle, an der man einen vergisst. */
+ function restoreState(d) {
+ restoring = true;
+ try {
+ takeState(kopie(d));
+ deck.dataset.view = view;
+ press("variants", "v", variant);
+ press("tilts", "t", tilt);
+ press("quality", "q", String(superSample));
+ press("conts", "c", continuous);
+ press("tagouts", "o", tagOut);
+ press("frames", "f", frameStyle);
+ press("views", "w", view);
+ SLIDES.forEach((s, i) => { if (s.type === "title") rebuildPage(i); });
+ applyAll();
+ applyEditsAll();
+ show(current);
+ if (picked) pick(picked.slide, picked.key); else syncEditPanel();
+ } finally { restoring = false; }
+ // Der zurueckgespielte Stand ist ab jetzt der Bezugspunkt, sonst legt der
+ // naechste Speichervorgang ihn gleich wieder als Schritt ab.
+ lastSnap = kopie(currentState());
+ saveState();
+ }
+
+ function undo() {
+ if (stateLocked) return;
+ if (!undoStack.length) { setStatus("Nichts mehr rueckgaengig zu machen", "err"); return; }
+ redoStack.push(kopie(currentState()));
+ restoreState(undoStack.pop());
+ setStatus("Rueckgaengig · noch " + undoStack.length + " Schritte", "ok");
+ }
+
+ function redo() {
+ if (stateLocked) return;
+ if (!redoStack.length) { setStatus("Nichts wiederherzustellen", "err"); return; }
+ undoStack.push(kopie(currentState()));
+ restoreState(redoStack.pop());
+ setStatus("Wiederhergestellt · noch " + redoStack.length + " Schritte", "ok");
+ }
+
function saveState() {
if (stateLocked) return;
const state = currentState();
+ if (!restoring) merkeFuerUndo(state);
try { localStorage.setItem(STORE, JSON.stringify(state)); }
catch (err) { console.warn("Speichern fehlgeschlagen", err); }
markSaved();
@@ -1222,6 +1331,14 @@
// sonst bliebe ein ueber Wochen gewachsener Stand ungeteilt liegen.
// Nach einer Angleichung ebenso, damit sie nicht bei jedem Laden erneut greift.
if (!fromFile || migrated) setTimeout(() => pushState(currentState()), 0);
+ takeState(d);
+ return true;
+ }
+
+ /* Einen Stand in die laufenden Variablen uebernehmen. Steht bewusst als
+ 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) {
// 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) {
@@ -1241,7 +1358,6 @@
tagOut = d.tagOut || tagOut;
frameStyle = d.frameStyle || frameStyle;
current = Number.isInteger(d.current) ? d.current : 0;
- return true;
}
let savedTimer = null;
@@ -2532,10 +2648,6 @@
document.getElementById("std-list").innerHTML = html;
})();
- document.getElementById("tagouts").addEventListener("click", e => {
- const b = e.target.closest("button");
- if (b) { tagOut = b.dataset.o; press("tagouts", "o", tagOut); applyAll(); saveState(); }
- });
document.getElementById("views").addEventListener("click", e => {
const b = e.target.closest("button");
if (b) { view = b.dataset.w; deck.dataset.view = view; press("views", "w", view); zoom = null; show(current); saveState(); }
@@ -2648,6 +2760,10 @@
const b = e.target.closest("button");
if (b) { bgState[current].int = b.dataset.i; syncBgButtons(); applyAll(); saveState(); }
});
+ document.getElementById("tagouts").addEventListener("click", e => {
+ const b = e.target.closest("button");
+ if (b) { tagOut = b.dataset.o; press("tagouts", "o", tagOut); applyAll(); saveState(); }
+ });
document.getElementById("frames").addEventListener("click", e => {
const b = e.target.closest("button");
if (b) { frameStyle = b.dataset.f; press("frames", "f", frameStyle); applyAll(); saveState(); }
@@ -2666,6 +2782,19 @@
document.getElementById("dl-all").addEventListener("click", guard(downloadAll));
const BG_CYCLE = ["formen", "formen-g", "nur-g", "waben", "punkte", "raster", "aus"];
+ /* ⌘/Strg + Z. Eigener Zuhoerer, weil der allgemeine unten alles mit
+ Sondertaste durchlaesst. In Eingabefeldern hat das Zurueckgehen des
+ Browsers Vorrang, sonst kaeme man aus einem Tippfehler nicht mehr heraus. */
+ document.addEventListener("keydown", e => {
+ if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
+ const k = e.key.toLowerCase();
+ if (k !== "z" && k !== "y") return;
+ const t = e.target;
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
+ e.preventDefault();
+ if (k === "y" || e.shiftKey) redo(); else undo();
+ });
+
document.addEventListener("keydown", e => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
// In einem Eingabefeld gehoeren die Tasten dem Feld, nicht den Kuerzeln.