Ausrichten im Bearbeiten-Modus
Elemente liessen sich nur mit der Maus schieben. Damit war ein Wert auf zwei Slides nie exakt gleich zu treffen, und genau das war die Aufgabe. Zahlen sind jetzt Eingabefelder. Breite oder Schrift, X und Y. Das Feld wird beim Ziehen nicht neu aufgebaut und nie das ueberschrieben, in dem der Cursor steht; Tastenkuerzel greifen in Eingabefeldern nicht mehr. X und Y zaehlen ab der Mitte der Slide und beziehen sich auf die OBERE LINKE ECKE des Elements, nicht auf seinen Mittelpunkt. Eine zweizeilige und eine dreizeilige Ueberschrift sind unterschiedlich hoch: bei gleichem Mittelpunkt liegen ihre Oberkanten 39 px auseinander und der hoehere Block waechst nach oben ins Tag. Ab der ersten Zeile gerechnet stimmt es unabhaengig von der Zeilenzahl. Zwei Knoepfe uebertragen Position oder Groesse des gewaehlten Elements auf alle anderen Slides. Angeglichen wird die Stelle, nicht der Versatz: jedes Element liegt von sich aus woanders. In der Einzelansicht wird jeder Holder kurz sichtbar gemacht, sonst hat er kein Layout. Hilfslinien beim Ziehen: Raster, Innenabstand und die beiden Mittelachsen, nur auf der Slide, an der gezogen wird. Liegt die Mitte auf einer Achse, leuchtet sie auf. Vor dem Export werden sie zwangsweise abgeschaltet. Merkzettel unten rechts: ein Stand laesst sich ablegen und mit einem Klick zurueckholen. Er liegt als eigenes Projekt in state/ und damit im Repo. Einmalige Angleichung der Tags beim Laden (Stand 1 auf 2), weil der Tag auf jeder Slide einzeln gezogen worden war. Das laeuft im Ladepfad und nicht ueber die Datei: ein offener Tab haelt seinen Stand im Speicher und schreibt eine Aenderung an der Datei beim naechsten Klick wieder zu. Ueberschrift der Chat-Slide von "automatisch seinen Kanal." auf "von selbst seinen Kanal.". Die alte Zeile misst mit Marker 933 px bei 896 verfuegbaren und brach in eine dritte Zeile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3832f82a65
commit
38c256e52f
1 file changed
+354
-11
+354
-11
@@ -129,6 +129,23 @@
|
||||
padding: 9px 11px; margin-bottom: 9px; min-height: 56px;
|
||||
}
|
||||
.edit-info .dim { color: #6E7480; }
|
||||
/* Ziehen ist gut fuer grob, tippen fuer genau. Ohne Eingabefelder laesst
|
||||
sich ein Wert nicht ueber mehrere Slides hinweg exakt gleich setzen. */
|
||||
.edit-info .fields {
|
||||
display: grid; grid-template-columns: 42px 1fr 20px 1fr;
|
||||
gap: 6px 7px; align-items: center; margin-top: 8px;
|
||||
}
|
||||
.edit-info label { color: #6E7480; font-size: 11px; }
|
||||
.edit-info input {
|
||||
width: 100%; box-sizing: border-box;
|
||||
background: #14161C; border: 1px solid #343845; border-radius: 6px;
|
||||
color: #E7EAF0; font: inherit; font-variant-numeric: tabular-nums;
|
||||
padding: 4px 6px; text-align: right; -moz-appearance: textfield;
|
||||
}
|
||||
.edit-info input::-webkit-outer-spin-button,
|
||||
.edit-info input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
.edit-info input:focus { outline: none; border-color: var(--accent-yellow); }
|
||||
.edit-info .hint { color: #6E7480; margin-top: 6px; }
|
||||
.std { margin-bottom: 22px; }
|
||||
.std summary {
|
||||
cursor: pointer; font-size: 11px; font-weight: 700; letter-spacing: .09em;
|
||||
@@ -191,6 +208,17 @@
|
||||
}
|
||||
.zoom button:hover { background: #262932; }
|
||||
.zoom .level { min-width: 58px; text-align: center; color: #8A8F9B; font-variant-numeric: tabular-nums; }
|
||||
/* Merkzettel neben der Zoomleiste: ein Stand, den man von Hand ablegt und
|
||||
bewusst zurueckholt. Der laufende Projektstand speichert dauernd von
|
||||
selbst und taugt deshalb nicht als Rueckfallpunkt. */
|
||||
.snap { display: flex; align-items: center; gap: 4px; margin-right: 10px; }
|
||||
.snap button {
|
||||
font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
|
||||
background: #1D1F26; color: #C7CCD6; border: 1px solid #2E313B;
|
||||
border-radius: 7px; padding: 6px 11px;
|
||||
}
|
||||
.snap button:hover { background: #262932; }
|
||||
.snap button:disabled { opacity: .4; cursor: default; }
|
||||
/* Der Streifen zeigt alle Slides nebeneinander und lückenlos, damit der
|
||||
durchgehende Aufbau beim Arbeiten sichtbar ist. Der Rahmen reserviert die
|
||||
skalierte Fläche; die Slides selbst bleiben immer echte 1080 x 1350. */
|
||||
@@ -354,6 +382,35 @@
|
||||
.slide[data-editmode="an"] [data-edit]:hover { outline: 3px dashed rgba(241,183,41,.75); outline-offset: 6px; }
|
||||
.slide[data-editmode="an"] [data-edit].picked { outline: 4px solid var(--accent-yellow); outline-offset: 6px; }
|
||||
|
||||
/* ─── Hilfslinien beim Ziehen ────────────────────────────────────────────
|
||||
Nur solange gezogen wird und nur auf der Slide, an der gezogen wird: das
|
||||
Raster deckt die 1080 × 1350 ab, nicht das Canvas. Ein Raster über alle
|
||||
Slides hinweg hilft nicht, weil jede Slide ihre eigene Bühne ist.
|
||||
pointer-events bleiben aus, sonst reisst die Ebene den Zug ab. */
|
||||
.guides { position: absolute; inset: 0; z-index: 9; display: none; pointer-events: none; }
|
||||
.slide[data-guides="an"] .guides { display: block; }
|
||||
/* 90 px = 1080/12 = 1350/15, teilt beide Kanten ohne Rest. */
|
||||
.guides .grid {
|
||||
position: absolute; inset: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(to right, var(--guide-line) 0 1px, transparent 1px 90px),
|
||||
repeating-linear-gradient(to bottom, var(--guide-line) 0 1px, transparent 1px 90px);
|
||||
}
|
||||
/* Der Innenabstand aus den Regeln, damit man sieht, wann Text ihn verlässt. */
|
||||
.guides .safe { position: absolute; inset: 92px; border: 2px dashed var(--guide-safe); }
|
||||
.guides .ax { position: absolute; background: var(--guide-axis); }
|
||||
.guides .ax-v { left: 50%; top: 0; bottom: 0; width: 2px; margin-left: -1px; }
|
||||
.guides .ax-h { top: 50%; left: 0; right: 0; height: 2px; margin-top: -1px; }
|
||||
/* Liegt die Mitte des Elements auf der Achse, wird sie kräftig: das ist die
|
||||
Rückmeldung, auf die es beim Zentrieren ankommt. */
|
||||
.guides .ax.on { background: var(--accent-yellow); }
|
||||
.guides .ax-v.on { width: 4px; margin-left: -2px; }
|
||||
.guides .ax-h.on { height: 4px; margin-top: -2px; }
|
||||
/* Fadenkreuz des gezogenen Elements, in Slide-Koordinaten gesetzt. */
|
||||
.guides .mid { position: absolute; background: var(--guide-mid); }
|
||||
.guides .mid-v { top: 0; bottom: 0; width: 2px; margin-left: -1px; }
|
||||
.guides .mid-h { left: 0; right: 0; height: 2px; margin-top: -1px; }
|
||||
|
||||
/* Jede Slide trägt nicht nur ihren eigenen Inhalt, sondern auch den ihrer
|
||||
Nachbarn, verschoben um deren Position. Dadurch läuft ein nach links
|
||||
geschobenes Element aus Slide 3 tatsächlich in Slide 2 hinein, statt am
|
||||
@@ -738,6 +795,10 @@
|
||||
--shape-accent-2: rgba(255,255,255,.14);
|
||||
--pat-color: #FFFFFF; --pat-strength: .07;
|
||||
--gmark-color: rgba(255,255,255,.13);
|
||||
--guide-line: rgba(255,255,255,.16);
|
||||
--guide-safe: rgba(255,255,255,.26);
|
||||
--guide-axis: rgba(255,255,255,.40);
|
||||
--guide-mid: rgba(241,183,41,.80);
|
||||
}
|
||||
.slide[data-variant="dunkel"] .headline .a,
|
||||
.slide[data-variant="dunkel"] .title-head .a { color: #1A1A1A; }
|
||||
@@ -756,6 +817,10 @@
|
||||
--shape-accent-2: rgba(48,49,61,.16);
|
||||
--pat-color: #30313D; --pat-strength: .07;
|
||||
--gmark-color: rgba(48,49,61,.16);
|
||||
--guide-line: rgba(48,49,61,.20);
|
||||
--guide-safe: rgba(48,49,61,.30);
|
||||
--guide-axis: rgba(48,49,61,.45);
|
||||
--guide-mid: rgba(200,144,26,.75);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -803,6 +868,12 @@
|
||||
<button class="btn secondary" id="sz-minus">A−</button>
|
||||
<button class="btn secondary" id="sz-plus">A+</button>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:8px">
|
||||
<button class="btn secondary" id="align-pos"
|
||||
title="Dieses Element auf allen Slides an dieselbe Stelle">Position angleichen</button>
|
||||
<button class="btn secondary" id="align-size"
|
||||
title="Dieses Element auf allen Slides gleich groß">Größe angleichen</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn secondary" id="reset-one">Element zurück</button>
|
||||
</div>
|
||||
@@ -914,6 +985,10 @@
|
||||
</div>
|
||||
<div class="hud">
|
||||
<p class="stage-meta" id="meta"></p>
|
||||
<div class="snap">
|
||||
<button id="snap-save" title="Diesen Stand als Rückfallpunkt ablegen">Stand merken</button>
|
||||
<button id="snap-load" title="Zum gemerkten Stand zurück" disabled>Zurück zum Stand</button>
|
||||
</div>
|
||||
<div class="zoom">
|
||||
<button id="zoom-out" title="Auszoomen">−</button>
|
||||
<button class="level" id="zoom-fit" title="Einpassen">Fit</button>
|
||||
@@ -999,7 +1074,10 @@
|
||||
key: "chat", type: "feature",
|
||||
layouts: [{ id: "standard", label: "Standard" }],
|
||||
eyebrow: "Team-Chat",
|
||||
head: 'Jedes Projekt bekommt<br>automatisch <span class="a">seinen Kanal.</span>',
|
||||
// "automatisch seinen Kanal." misst mit Marker 933 px bei 896
|
||||
// verfuegbaren und bricht deshalb in eine dritte Zeile. "von selbst"
|
||||
// sagt dasselbe und bleibt mit 846 px in der Zeile.
|
||||
head: 'Jedes Projekt bekommt<br>von selbst <span class="a">seinen Kanal.</span>',
|
||||
body: "Foto von der Baustelle rein, alle sehen's sofort. Aus jeder Nachricht wird mit einem Klick eine Aufgabe.",
|
||||
img: "./assets/screens/chat.png",
|
||||
mockW: 1030, mockAR: 1.703,
|
||||
@@ -1080,13 +1158,36 @@
|
||||
const STORE = "hivegrid.projekt." + PROJECT;
|
||||
const STATE_URL = "./state/" + PROJECT + ".json";
|
||||
|
||||
const STATE_V = 2;
|
||||
|
||||
function currentState() {
|
||||
return {
|
||||
v: 1, variant, tilt, superSample, continuous, view, tagOut, frameStyle,
|
||||
v: STATE_V, variant, tilt, superSample, continuous, view, tagOut, frameStyle,
|
||||
current, layoutState, bgState, edits,
|
||||
};
|
||||
}
|
||||
|
||||
/* ─── Angleichung des Tags (einmalig, Stand 1 → 2) ───────────────────────
|
||||
Der Tag war auf jeder Slide einzeln gezogen worden und stand deshalb
|
||||
überall etwas anders. Er soll auf allen Feature-Slides denselben Abstand
|
||||
zur oberen und linken Kante haben; Maß ist die erste Feature-Slide.
|
||||
Das läuft hier und nicht über die Datei: solange ein Tab offen ist, hält
|
||||
der seinen Stand im Speicher und schreibt jede Änderung an der Datei beim
|
||||
nächsten Klick wieder zu. Im Ladepfad kann er das nicht. */
|
||||
function migrateState(d) {
|
||||
if (!d || (d.v || 1) >= 2) return false;
|
||||
const feat = SLIDES.map((s, i) => (s.type === "feature" ? i : -1)).filter(i => i >= 0);
|
||||
const ref = feat.length && Array.isArray(d.edits) ? (d.edits[feat[0]] || {}).badge : null;
|
||||
if (ref) {
|
||||
for (const i of feat.slice(1)) {
|
||||
const e = d.edits[i] || (d.edits[i] = {});
|
||||
e.badge = Object.assign({}, e.badge, { dx: ref.dx || 0, dy: ref.dy || 0 });
|
||||
}
|
||||
}
|
||||
d.v = 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Der Stand geht in zwei Ablagen. Sofort in den localStorage, damit die
|
||||
Rückmeldung nicht auf das Netz wartet, und gebündelt in
|
||||
state/<projekt>.json. Nur die Datei lässt sich pushen, also ist sie die
|
||||
@@ -1132,10 +1233,12 @@
|
||||
if (!d) {
|
||||
try { d = JSON.parse(localStorage.getItem(STORE) || "null"); } catch (_) {}
|
||||
}
|
||||
if (!d || d.v !== 1) return false;
|
||||
if (!d || !(d.v === 1 || d.v === 2)) return false;
|
||||
const migrated = migrateState(d);
|
||||
// Stand aus dem Browser, aber noch keine Datei: einmalig hinueberschreiben,
|
||||
// sonst bliebe ein ueber Wochen gewachsener Stand ungeteilt liegen.
|
||||
if (!fromFile) setTimeout(() => pushState(currentState()), 0);
|
||||
// Nach einer Angleichung ebenso, damit sie nicht bei jedem Laden erneut greift.
|
||||
if (!fromFile || migrated) setTimeout(() => pushState(currentState()), 0);
|
||||
// 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) {
|
||||
@@ -1543,9 +1646,48 @@
|
||||
sheet.appendChild(buildPage(j));
|
||||
}
|
||||
el.appendChild(sheet);
|
||||
// Über allem, damit die Linien auch auf dem Mockup lesbar bleiben.
|
||||
el.insertAdjacentHTML("beforeend",
|
||||
'<div class="guides"><div class="grid"></div><div class="safe"></div>' +
|
||||
'<div class="mid mid-v"></div><div class="mid mid-h"></div>' +
|
||||
'<div class="ax ax-v"></div><div class="ax ax-h"></div></div>');
|
||||
return el;
|
||||
}
|
||||
|
||||
/* ─── Hilfslinien ────────────────────────────────────────────────────────
|
||||
Die Linien gehören zur Slide, nicht zum Canvas: gemessen wird deshalb
|
||||
gegen deren Rechteck. Der Maßstab kürzt sich dabei heraus, die Werte sind
|
||||
also unabhängig vom Zoom. */
|
||||
const SNAP = 4; // px in Slide-Koordinaten
|
||||
let guideSlide = null;
|
||||
|
||||
function showGuides(slideEl) {
|
||||
guideSlide = slideEl;
|
||||
slideEl.dataset.guides = "an";
|
||||
}
|
||||
|
||||
function hideGuides() {
|
||||
if (!guideSlide) return;
|
||||
delete guideSlide.dataset.guides;
|
||||
guideSlide.querySelectorAll(".guides .on").forEach(n => n.classList.remove("on"));
|
||||
guideSlide = null;
|
||||
}
|
||||
|
||||
function updateGuides(node) {
|
||||
if (!guideSlide) return;
|
||||
const s = guideSlide.getBoundingClientRect();
|
||||
const n = node.getBoundingClientRect();
|
||||
if (!s.width || !n.width) return;
|
||||
const k = s.width / 1080;
|
||||
const cx = (n.left + n.width / 2 - s.left) / k;
|
||||
const cy = (n.top + n.height / 2 - s.top) / k;
|
||||
const g = guideSlide.querySelector(".guides");
|
||||
g.querySelector(".mid-v").style.left = cx + "px";
|
||||
g.querySelector(".mid-h").style.top = cy + "px";
|
||||
g.querySelector(".ax-v").classList.toggle("on", Math.abs(cx - 540) <= SNAP);
|
||||
g.querySelector(".ax-h").classList.toggle("on", Math.abs(cy - 675) <= SNAP);
|
||||
}
|
||||
|
||||
/* Every asset is inlined as a data URL once, before the deck is built.
|
||||
Same-origin already satisfies the canvas, but inlining removes every fetch
|
||||
and timing failure mode from the export path, and it lets a layout switch
|
||||
@@ -1873,6 +2015,9 @@
|
||||
}
|
||||
|
||||
async function renderSlide(i) {
|
||||
// Hilfslinien sind Werkzeug: koennten sie aus einem Tastendruck noch
|
||||
// stehen, landeten sie im Bild.
|
||||
clearTimeout(nudgeTimer); hideGuides();
|
||||
const holder = deck.querySelectorAll(".holder")[i];
|
||||
const el = holder.querySelector(".slide");
|
||||
// In der Einzelansicht sind die übrigen Holder ausgeblendet; zum Messen und
|
||||
@@ -2047,15 +2192,19 @@
|
||||
const startX = e.clientX, startY = e.clientY;
|
||||
const baseX = set.dx || 0, baseY = set.dy || 0;
|
||||
node.style.cursor = "grabbing";
|
||||
showGuides(holder.querySelector(".slide"));
|
||||
updateGuides(node);
|
||||
|
||||
const move = ev => {
|
||||
set.dx = Math.round(baseX + (ev.clientX - startX) / k);
|
||||
set.dy = Math.round(baseY + (ev.clientY - startY) / k);
|
||||
applyEditsAll(); // zieht alle Kopien der Seite mit
|
||||
updateGuides(node);
|
||||
syncEditPanel();
|
||||
};
|
||||
const up = () => {
|
||||
node.style.cursor = "";
|
||||
hideGuides();
|
||||
document.removeEventListener("mousemove", move);
|
||||
document.removeEventListener("mouseup", up);
|
||||
saveState();
|
||||
@@ -2072,6 +2221,7 @@
|
||||
syncEditPanel();
|
||||
}
|
||||
|
||||
let nudgeTimer = null;
|
||||
function nudge(dx, dy) {
|
||||
if (!picked) return;
|
||||
const set = (edits[picked.slide][picked.key] = edits[picked.slide][picked.key] || {});
|
||||
@@ -2079,6 +2229,17 @@
|
||||
set.dy = (set.dy || 0) + dy;
|
||||
applyEditsAll();
|
||||
pick(picked.slide, picked.key);
|
||||
// Pixelweise schieben braucht dieselbe Rueckmeldung wie das Ziehen, nur
|
||||
// soll sie danach von selbst verschwinden.
|
||||
const holder = deck.querySelectorAll(".holder")[current];
|
||||
const node = holder && holder.querySelector('.page[data-j="' + picked.slide +
|
||||
'"] [data-edit="' + picked.key + '"]');
|
||||
if (node) {
|
||||
showGuides(holder.querySelector(".slide"));
|
||||
updateGuides(node);
|
||||
clearTimeout(nudgeTimer);
|
||||
nudgeTimer = setTimeout(hideGuides, 900);
|
||||
}
|
||||
saveState();
|
||||
}
|
||||
|
||||
@@ -2112,19 +2273,98 @@
|
||||
setStatus("Alle Slides auf Standard zurückgesetzt", "ok");
|
||||
}
|
||||
|
||||
/* X und Y zaehlen ab der Mitte der Slide, nicht ab der Stelle, an der das
|
||||
Element von sich aus laege. Nur so bedeuten gleiche Werte bei zwei
|
||||
verschiedenen Elementen auch dieselbe Stelle.
|
||||
Gemessen wird die OBERE LINKE ECKE, nicht der Mittelpunkt. Eine
|
||||
zweizeilige und eine dreizeilige Ueberschrift sind unterschiedlich hoch;
|
||||
bei gleichem Mittelpunkt liegen ihre Oberkanten 39 px auseinander und der
|
||||
hoehere Block waechst nach oben ins Tag. Ab der ersten Zeile gerechnet
|
||||
stimmt es unabhaengig von der Zeilenzahl.
|
||||
Gemessen wird die erste sichtbare Kopie: in der Einzelansicht sind die
|
||||
uebrigen Holder ausgeblendet und liefern ein leeres Rechteck. */
|
||||
function centreOf(slideIdx, key) {
|
||||
const nodes = deck.querySelectorAll(
|
||||
'.page[data-j="' + slideIdx + '"] [data-edit="' + key + '"]');
|
||||
for (const n of nodes) {
|
||||
const r = n.getBoundingClientRect();
|
||||
if (!r.width) continue;
|
||||
const holder = n.closest(".slide");
|
||||
const sr = holder && holder.getBoundingClientRect();
|
||||
if (!sr || !sr.width) continue;
|
||||
const k = sr.width / 1080;
|
||||
return {
|
||||
x: Math.round((r.left - sr.left) / k - 540),
|
||||
y: Math.round((r.top - sr.top) / k - 675),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncEditPanel() {
|
||||
const box = document.getElementById("edit-info");
|
||||
if (!picked) { box.innerHTML = '<span class="dim">Nichts ausgewählt</span>'; return; }
|
||||
if (!picked) {
|
||||
box.dataset.for = "";
|
||||
box.innerHTML = '<span class="dim">Nichts ausgewählt</span>';
|
||||
return;
|
||||
}
|
||||
const std = STANDARD[picked.key] || {};
|
||||
const e = (edits[picked.slide] || {})[picked.key] || {};
|
||||
const base = stdSize(picked.key, picked.slide);
|
||||
const val = e.size || base;
|
||||
const unit = std.prop === "font-size" ? "px Schrift" : "px breit";
|
||||
box.innerHTML =
|
||||
'<b>' + (std.label || picked.key) + '</b> · Slide ' + (picked.slide + 1) + '<br>' +
|
||||
(std.prop ? val + " " + unit + (e.size ? " (Standard " + base + ")" : "") + "<br>" : "") +
|
||||
"Versatz " + (e.dx || 0) + " / " + (e.dy || 0) + " px";
|
||||
const id = picked.slide + ":" + picked.key;
|
||||
// Nur bei einer anderen Auswahl neu aufbauen. Beim Ziehen laeuft das hier
|
||||
// pro Mausbewegung; ein Neuaufbau wuerde die Eingabe unter den Fingern
|
||||
// wegreissen.
|
||||
if (box.dataset.for !== id) {
|
||||
box.dataset.for = id;
|
||||
box.innerHTML =
|
||||
'<b>' + (std.label || picked.key) + '</b> · Slide ' + (picked.slide + 1) +
|
||||
'<div class="fields">' +
|
||||
(std.prop
|
||||
? '<label for="f-size">' + (std.prop === "font-size" ? "Schrift" : "Breite") +
|
||||
'</label><input id="f-size" type="number" step="1" style="grid-column:2/5">'
|
||||
: '') +
|
||||
'<label for="f-dx">X</label><input id="f-dx" type="number" step="1">' +
|
||||
'<label for="f-dy">Y</label><input id="f-dy" type="number" step="1">' +
|
||||
'</div>' +
|
||||
'<div class="hint">' + (std.prop ? 'Standard ' + base + ' px · ' : '') +
|
||||
'Obere linke Ecke, 0 / 0 ist die Mitte der Slide</div>';
|
||||
}
|
||||
const put = (sel, v) => {
|
||||
const el = box.querySelector(sel);
|
||||
if (el && document.activeElement !== el) el.value = v;
|
||||
};
|
||||
if (std.prop) put("#f-size", e.size || base);
|
||||
const c = centreOf(picked.slide, picked.key);
|
||||
if (c) { put("#f-dx", c.x); put("#f-dy", c.y); }
|
||||
}
|
||||
|
||||
/* Eingabe wirkt sofort. Kein pick() danach: das baut das Feld neu auf und
|
||||
der Fokus waere weg. */
|
||||
document.getElementById("edit-info").addEventListener("input", ev => {
|
||||
const el = ev.target;
|
||||
if (!picked || !el.matches("input")) return;
|
||||
const raw = el.value.trim();
|
||||
const n = Number(raw);
|
||||
if (raw !== "" && !Number.isFinite(n)) return;
|
||||
const set = (edits[picked.slide][picked.key] = edits[picked.slide][picked.key] || {});
|
||||
if (el.id === "f-dx" || el.id === "f-dy") {
|
||||
// Der Wert ist eine Stelle auf der Slide, kein Verschiebebetrag. Der
|
||||
// Unterschied zur jetzigen Stelle wandert deshalb in den Versatz.
|
||||
const c = centreOf(picked.slide, picked.key);
|
||||
if (!c) return;
|
||||
const ziel = raw === "" ? 0 : Math.round(n);
|
||||
if (el.id === "f-dx") set.dx = (set.dx || 0) + (ziel - c.x);
|
||||
else set.dy = (set.dy || 0) + (ziel - c.y);
|
||||
}
|
||||
// Leere Groesse heisst Standard, nicht null.
|
||||
else if (el.id === "f-size") {
|
||||
if (raw === "") delete set.size;
|
||||
else set.size = Math.max(8, Math.round(n));
|
||||
}
|
||||
applyEditsAll();
|
||||
saveState();
|
||||
});
|
||||
/* Welche Kategorien offen sind, ist Bedienvorliebe und gehört nicht in den
|
||||
Projektstand: gilt für beide Projekte gleichermaßen. */
|
||||
(function rememberGroups() {
|
||||
@@ -2157,6 +2397,107 @@
|
||||
document.getElementById("sz-plus").addEventListener("click", () => resize(1));
|
||||
document.getElementById("sz-minus").addEventListener("click", () => resize(-1));
|
||||
document.getElementById("reset-one").addEventListener("click", resetOne);
|
||||
/* ─── Angleichen über alle Slides ────────────────────────────────────────
|
||||
Das gewählte Element ist die Vorlage, alle gleichnamigen auf den anderen
|
||||
Slides ziehen nach. Angeglichen wird die Stelle, nicht der Versatz: jedes
|
||||
Element liegt von sich aus woanders, und nur die Stelle ist vergleichbar.
|
||||
In der Einzelansicht haben die übrigen Holder kein Layout, weil sie
|
||||
ausgeblendet sind. Jeder wird deshalb kurz sichtbar gemacht — derselbe
|
||||
Weg, den auch der Export geht. */
|
||||
function alignAll(was) {
|
||||
if (!picked) return;
|
||||
const key = picked.key;
|
||||
const ziel = was === "size" ? null : centreOf(picked.slide, key);
|
||||
if (was !== "size" && !ziel) { setStatus("Element nicht messbar", "err"); return; }
|
||||
const size = (edits[picked.slide][key] || {}).size;
|
||||
const holders = deck.querySelectorAll(".holder");
|
||||
let n = 0;
|
||||
SLIDES.forEach((_, i) => {
|
||||
if (i === picked.slide) return;
|
||||
const holder = holders[i];
|
||||
if (!holder || !holder.querySelector('.page[data-j="' + i + '"] [data-edit="' + key + '"]')) return;
|
||||
const restore = holder.style.display;
|
||||
holder.style.display = "block";
|
||||
const set = (edits[i][key] = edits[i][key] || {});
|
||||
if (was === "size") {
|
||||
if (size == null) delete set.size; else set.size = size;
|
||||
} else {
|
||||
const c = centreOf(i, key);
|
||||
if (c) {
|
||||
set.dx = (set.dx || 0) + (ziel.x - c.x);
|
||||
set.dy = (set.dy || 0) + (ziel.y - c.y);
|
||||
}
|
||||
}
|
||||
holder.style.display = restore;
|
||||
n++;
|
||||
});
|
||||
applyEditsAll();
|
||||
syncEditPanel();
|
||||
saveState();
|
||||
const label = (STANDARD[key] || {}).label || key;
|
||||
setStatus(label + ": " + (was === "size" ? "Größe" : "Position") +
|
||||
" auf " + n + " weitere Slide" + (n === 1 ? "" : "s") + " übertragen", "ok");
|
||||
}
|
||||
document.getElementById("align-pos").addEventListener("click", () => alignAll("pos"));
|
||||
document.getElementById("align-size").addEventListener("click", () => alignAll("size"));
|
||||
|
||||
/* ─── Merkzettel ─────────────────────────────────────────────────────────
|
||||
Der gemerkte Stand liegt als eigenes Projekt in state/, also im Repo und
|
||||
damit auch auf der anderen Maschine. Zurueckgeholt wird er, indem er zum
|
||||
laufenden Stand gemacht und die Seite neu geladen wird: dann greift
|
||||
derselbe Ladepfad wie sonst auch, statt dass ich hier von Hand jede
|
||||
Einstellung nachziehe und dabei eine vergesse. */
|
||||
const SNAP_PROJECT = PROJECT + "-merk";
|
||||
const snapSave = document.getElementById("snap-save");
|
||||
const snapLoad = document.getElementById("snap-load");
|
||||
|
||||
async function snapExists() {
|
||||
try {
|
||||
const res = await fetch("./state/" + SNAP_PROJECT + ".json", { cache: "no-store" });
|
||||
return res.ok;
|
||||
} catch (_) { return false; }
|
||||
}
|
||||
snapExists().then(ok => { snapLoad.disabled = !ok; });
|
||||
|
||||
snapSave.addEventListener("click", async () => {
|
||||
snapSave.disabled = true;
|
||||
try {
|
||||
const res = await fetch("/state?project=" + SNAP_PROJECT, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(currentState()),
|
||||
});
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
snapLoad.disabled = false;
|
||||
setStatus("Stand gemerkt", "ok");
|
||||
} catch (err) {
|
||||
setStatus("Merken fehlgeschlagen: " + err.message, "err");
|
||||
} finally { snapSave.disabled = false; }
|
||||
});
|
||||
|
||||
snapLoad.addEventListener("click", async () => {
|
||||
snapLoad.disabled = true;
|
||||
try {
|
||||
const res = await fetch("./state/" + SNAP_PROJECT + ".json", { cache: "no-store" });
|
||||
if (!res.ok) throw new Error("kein gemerkter Stand");
|
||||
const d = await res.json();
|
||||
// Erst den laufenden Timer entschaerfen, sonst schreibt er beim Entladen
|
||||
// den alten Stand ueber den gerade zurueckgeholten.
|
||||
clearTimeout(pushTimer); pushTimer = null;
|
||||
const put = await fetch("/state?project=" + PROJECT, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(d),
|
||||
});
|
||||
if (!put.ok) throw new Error("HTTP " + put.status);
|
||||
try { localStorage.setItem(STORE, JSON.stringify(d)); } catch (_) {}
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
snapLoad.disabled = false;
|
||||
setStatus("Zurueckholen fehlgeschlagen: " + err.message, "err");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("reset-all").addEventListener("click", resetAll);
|
||||
|
||||
// Übersicht der Standards, direkt aus derselben Tabelle wie der Reset.
|
||||
@@ -2319,6 +2660,8 @@
|
||||
const BG_CYCLE = ["formen", "formen-g", "nur-g", "waben", "punkte", "raster", "aus"];
|
||||
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;
|
||||
const k = e.key.toLowerCase();
|
||||
if (editMode === "an" && picked && e.key.startsWith("Arrow")) {
|
||||
const step = e.shiftKey ? 10 : 1;
|
||||
|
||||
Reference in new issue
Block a user