Rueckgaengig, Slide-Tausch und ausgeblendete Tags

Der Commit buendelt zwei Arbeitsstraenge, die in derselben Datei entstanden
sind und sich nicht sauber trennen liessen.

Aus der parallelen Sitzung: Rueckgaengig und Wiederherstellen ueber Cmd/Strg+Z,
angehaengt an saveState statt an die 25 Stellen, die etwas aendern. Ziehen
speichert erst beim Loslassen, eine Bewegung ist also ein Schritt und nicht
hundert. Dazu der Tausch von Automation & KI und Vertrieb samt Stand 4: der
Stand haengt an der Position, nicht am Slide-Schluessel, ohne Tausch laegen
Positionen, Muster und Layout an der falschen Slide.

Neu hier: die Tags sind ausgeblendet, nicht entfernt. Beim ersten Versuch
waren sie geloescht, worauf Ueberschrift und Text nach oben rutschten - das
Tag steht im Textfluss und belegt Hoehe. visibility haelt die Box, display
none haette sie ebenso aufgegeben. Der Umschalter fuer die Ausrueckung und
sein Kuerzel bleiben deshalb bestehen.

Stand 5 raeumt den gespeicherten Tag-Eintrag: seine Stelle ist ohne Wirkung,
und die Werte standen zuletzt bis 141 px oberhalb der Slide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-07 01:13:57 +02:00
1 parent 8daa1ac3a6
commit 94dbd5c7d4
1 file changed
+155 -26
+155 -26
View File
@@ -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 @@
<kbd>E</kbd> Bearbeiten · Pfeile verschieben (<kbd>⇧</kbd> = 10 px)<br>
<kbd>+</kbd> <kbd>−</kbd> <kbd>0</kbd> Zoom · ⌘/Strg + Rad<br>
<kbd>Leer</kbd> + ziehen verschiebt die Fläche<br>
<kbd>⌘/Strg</kbd>+<kbd>Z</kbd> Rückgängig (<kbd>⇧</kbd> = wiederherstellen)<br>
<kbd>D</kbd> Slide laden · <kbd>A</kbd> alle als ZIP
</div>
</details>
@@ -976,24 +981,6 @@
head: 'Vom <span class="a">Handwerker</span><br>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,<br><span class="a">wo alle Auftr\u00e4ge stehen.</span>',
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,<br><span class="a">wo alle Auftr\u00e4ge stehen.</span>',
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.