Canvas-Bearbeitung, Projektspeicher und Arbeitsfläche
Der Generator ist von einer Vorschau zu einer Werkbank geworden: Slides lassen sich direkt im Canvas verschieben und skalieren, der Stand bleibt erhalten. - Streifenansicht mit allen Slides nebeneinander, Auswahl per Klick - Durchlaufendes Blatt: jede Slide trägt die Inhalte ihrer Nachbarn, damit verschobene Elemente über die Kante hinweg weiterlaufen - Bearbeiten im Canvas: ziehen, Schriftgrößen und Bildbreiten, Reset je Element oder komplett - Projektstand pro Post im Browser gespeichert, Navigation zwischen den Posts - Arbeitsfläche vom Bedienfeld getrennt, eigener Zoom und Panning - Bildrahmen als Chamfer, rund, Stapel oder aus; Schatten schaltbar - Screenshots zeigen den vollen Screen statt eines Ausschnitts - Kategorien in der Sidebar standardmäßig zugeklappt Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
124524482b
commit
170b72a538
2 files changed
+379
-66
No files matched your search
+17
-4
@@ -24,11 +24,11 @@
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body { overscroll-behavior: none; }
|
||||
body {
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
background: #17181F; color: #E8EAEE;
|
||||
min-height: 100vh; display: flex; gap: 40px; padding: 32px;
|
||||
align-items: flex-start;
|
||||
height: 100vh; overflow: hidden; display: flex;
|
||||
}
|
||||
|
||||
/* ─── Projekt-Navigation ─────────────────────────────────────────────── */
|
||||
@@ -50,7 +50,11 @@
|
||||
.forget:hover { color: #EB7C6B; }
|
||||
|
||||
/* ─── Control panel ──────────────────────────────────────────────────── */
|
||||
.panel { width: 300px; flex: none; position: sticky; top: 32px; }
|
||||
.panel {
|
||||
width: 306px; flex: none; height: 100vh; overflow-y: auto;
|
||||
padding: 28px 22px 40px 28px;
|
||||
border-right: 1px solid #24262F; background: #17181F;
|
||||
}
|
||||
.panel h1 { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 4px; }
|
||||
.panel .sub { font-size: 13px; color: #8A8F9B; margin-bottom: 24px; line-height: 1.5; }
|
||||
|
||||
@@ -95,7 +99,16 @@
|
||||
.status.err { color: #EB7C6B; }
|
||||
|
||||
/* ─── Stage ──────────────────────────────────────────────────────────── */
|
||||
.stage { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
||||
/* Arbeitsfläche, klar abgesetzt von der Bedienleiste. */
|
||||
.stage {
|
||||
flex: 1; position: relative; height: 100vh; overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
background-color: #101116;
|
||||
background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px);
|
||||
background-size: 24px 24px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 14px; padding: 48px;
|
||||
}
|
||||
.stage-meta { font-size: 12px; color: #6E7480; letter-spacing: .04em; }
|
||||
|
||||
/* The wrapper reserves the *scaled* footprint so the page doesn't jump;
|
||||
|
||||
+362
-62
@@ -26,11 +26,11 @@
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body { overscroll-behavior: none; }
|
||||
body {
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
background: #17181F; color: #E8EAEE;
|
||||
min-height: 100vh; display: flex; gap: 36px; padding: 28px;
|
||||
align-items: flex-start;
|
||||
height: 100vh; overflow: hidden; display: flex;
|
||||
}
|
||||
|
||||
/* ─── Projekt-Navigation ─────────────────────────────────────────────── */
|
||||
@@ -52,16 +52,37 @@
|
||||
.forget:hover { color: #EB7C6B; }
|
||||
|
||||
/* ─── Control panel ──────────────────────────────────────────────────── */
|
||||
.panel { width: 290px; flex: none; position: sticky; top: 28px; }
|
||||
.panel {
|
||||
width: 306px; flex: none; height: 100vh; overflow-y: auto;
|
||||
padding: 28px 22px 40px 28px;
|
||||
border-right: 1px solid #24262F; background: #17181F;
|
||||
}
|
||||
.panel h1 { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 4px; }
|
||||
.panel .sub { font-size: 13px; color: #8A8F9B; margin-bottom: 22px; line-height: 1.5; }
|
||||
.panel a { color: #8A8F9B; }
|
||||
|
||||
.group { margin-bottom: 20px; }
|
||||
.group > label {
|
||||
.group > label,
|
||||
.group > summary {
|
||||
display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.09em;
|
||||
text-transform: uppercase; color: #6E7480; margin-bottom: 9px;
|
||||
}
|
||||
/* Zugeklappte Kategorien: der Marker ersetzt den Standard-Pfeil, damit die
|
||||
Zeile bündig zur offenen Gruppe bleibt. */
|
||||
details.group > summary {
|
||||
cursor: pointer; list-style: none; margin-bottom: 0;
|
||||
padding: 5px 0; display: flex; align-items: center; gap: 7px;
|
||||
transition: color .15s;
|
||||
}
|
||||
details.group > summary::-webkit-details-marker { display: none; }
|
||||
details.group > summary::before {
|
||||
content: "▸"; font-size: 10px; color: #4E5360; transition: transform .15s;
|
||||
}
|
||||
details.group[open] > summary::before { transform: rotate(90deg); }
|
||||
details.group > summary:hover { color: #C7CCD6; }
|
||||
details.group[open] > summary { color: #8A8F9B; margin-bottom: 9px; }
|
||||
details.group { margin-bottom: 4px; }
|
||||
details.group[open] { margin-bottom: 20px; }
|
||||
|
||||
.opts { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
||||
.opts.one { grid-template-columns: 1fr; }
|
||||
@@ -123,13 +144,64 @@
|
||||
.status.err { color: #EB7C6B; }
|
||||
|
||||
/* ─── Stage ──────────────────────────────────────────────────────────── */
|
||||
.stage { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
/* ─── Arbeitsfläche ───────────────────────────────────────────────────────
|
||||
Klar abgesetzt von der Sidebar: eigener Grund, eigener Scroll, eigener
|
||||
Zoom. Was hier passiert, lässt die Bedienleiste unberührt. */
|
||||
.stage {
|
||||
flex: 1; position: relative; height: 100vh; overflow: hidden;
|
||||
background-color: #101116;
|
||||
background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
/* overscroll-behavior contain hält die Wisch-Geste des Browsers zurück:
|
||||
Ohne das springt horizontales Trackpad-Scrollen am linken Rand auf den
|
||||
vorigen Tab, statt im Canvas zu bleiben. */
|
||||
.scroller {
|
||||
position: absolute; inset: 0; overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.scroller.panning { cursor: grabbing; }
|
||||
.scroller.panready { cursor: grab; }
|
||||
/* width/height: max-content ist hier entscheidend. Mit reinem
|
||||
"justify-content: center" ragt breiter Inhalt über beide Seiten hinaus,
|
||||
aber scrollLeft kann nicht negativ werden: Der linke Teil wäre dann
|
||||
unerreichbar. So wächst die Fläche mit dem Inhalt, das Zentrieren greift
|
||||
nur noch, solange etwas Platz übrig ist. */
|
||||
.canvas {
|
||||
width: max-content; min-width: 100%;
|
||||
height: max-content; min-height: 100%;
|
||||
box-sizing: border-box; padding: 60px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
.hud {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 16px; padding: 14px 20px; pointer-events: none;
|
||||
background: linear-gradient(transparent, rgba(16,17,22,.92) 55%);
|
||||
}
|
||||
.hud > * { pointer-events: auto; }
|
||||
.stage-meta { font-size: 12px; color: #6E7480; letter-spacing: .04em; }
|
||||
|
||||
.zoom { display: flex; align-items: center; gap: 4px; }
|
||||
.zoom button {
|
||||
font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
|
||||
background: #1D1F26; color: #C7CCD6; border: 1px solid #2E313B;
|
||||
border-radius: 7px; padding: 6px 10px; min-width: 32px;
|
||||
}
|
||||
.zoom button:hover { background: #262932; }
|
||||
.zoom .level { min-width: 58px; text-align: center; color: #8A8F9B; font-variant-numeric: tabular-nums; }
|
||||
/* 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. */
|
||||
.strip-wrap { overflow: hidden; flex: none; align-self: flex-start; }
|
||||
.strip { display: flex; transform-origin: top left; }
|
||||
/* Bei gebrochenem Maßstab (etwa 41 %) ist eine Slide 442,8 px breit. Der
|
||||
Browser rundet jede Box einzeln, und zwischen ihnen blitzt für Sekunden-
|
||||
bruchteile eines Pixels der dunkle Grund durch. Der Streifen bekommt
|
||||
deshalb die Slide-Farbe als Untergrund: Die Fuge zeigt dann dieselbe Farbe
|
||||
und verschwindet. Im Export gibt es sie ohnehin nicht, dort wird jede
|
||||
Slide einzeln in exakten 1080 x 1350 gerendert. */
|
||||
.strip { display: flex; transform-origin: top left; background: #30313D; }
|
||||
.holder { position: relative; flex: none; cursor: pointer; }
|
||||
/* Auswahlrahmen sitzt auf dem Holder, nicht auf der Slide, damit er nie
|
||||
im Export landet. */
|
||||
@@ -345,6 +417,9 @@
|
||||
22px 0, calc(100% - 22px) 0, 100% 22px, 100% calc(100% - 22px),
|
||||
calc(100% - 22px) 100%, 22px 100%, 0 calc(100% - 22px), 0 22px
|
||||
);
|
||||
/* Rahmenform ist umschaltbar. Bei "rund" muss clip-path weichen, sonst
|
||||
gewinnt der Schnitt gegen border-radius; bei "aus" fällt auch Polsterung
|
||||
und Fläche weg, der Schatten bleibt als einzige Tiefe. */
|
||||
/* clip-path also clips box-shadow, so depth has to come from a filter —
|
||||
documented caveat in lib/chamfer.ts. */
|
||||
filter: drop-shadow(0 34px 60px rgba(0,0,0,.30));
|
||||
@@ -363,6 +438,57 @@
|
||||
calc(100% - 14px) 100%, 14px 100%, 0 calc(100% - 14px), 0 14px
|
||||
);
|
||||
}
|
||||
|
||||
.slide[data-frame="rund"] .mock { clip-path: none; border-radius: 30px; }
|
||||
.slide[data-frame="rund"] .mock-inner { clip-path: none; border-radius: 20px; }
|
||||
|
||||
.slide[data-frame="aus"] .mock {
|
||||
clip-path: none; padding: 0; background: none; border-radius: 0;
|
||||
}
|
||||
.slide[data-frame="aus"] .mock-inner { clip-path: none; border-radius: 0; }
|
||||
|
||||
/* ─── Stapel ─────────────────────────────────────────────────────────────
|
||||
Zwei versetzte Blätter hinter dem Screenshot. Der Rahmen selbst fällt weg,
|
||||
jedes Blatt trägt seinen eigenen Schatten: box-shadow statt drop-shadow,
|
||||
weil ohne clip-path nichts mehr beschnitten wird und jede Karte einzeln
|
||||
abheben soll. */
|
||||
.sheet-back, .sheet-mid {
|
||||
display: none; position: absolute; inset: 0; z-index: 0;
|
||||
background: #fff; border-radius: 22px; overflow: hidden;
|
||||
}
|
||||
.slide[data-frame="stapel"] .mock {
|
||||
clip-path: none; padding: 0; background: none; filter: none;
|
||||
}
|
||||
.slide[data-frame="stapel"] .mock-inner {
|
||||
clip-path: none; border-radius: 22px; z-index: 2;
|
||||
box-shadow: 0 34px 64px rgba(0,0,0,.30), 0 3px 10px rgba(0,0,0,.14);
|
||||
}
|
||||
.slide[data-frame="stapel"] .sheet-mid {
|
||||
display: block; z-index: 1;
|
||||
transform: rotate(-2.4deg) translate(-2.2%, 3%) scale(.975);
|
||||
box-shadow: 0 22px 44px rgba(0,0,0,.20);
|
||||
}
|
||||
.slide[data-frame="stapel"] .sheet-back {
|
||||
display: block;
|
||||
transform: rotate(3.2deg) translate(3%, 5.5%) scale(.95);
|
||||
box-shadow: 0 18px 38px rgba(0,0,0,.16);
|
||||
}
|
||||
/* Das hinterste Blatt zeigt bewusst einen anderen Screen: der Stapel soll
|
||||
nach mehreren Ansichten aussehen, nicht nach einer Kopie. */
|
||||
.sheet-back img {
|
||||
position: absolute; top: 0; left: 0; width: 100%;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
/* Schatten global abschaltbar. Zwei Techniken im Spiel: drop-shadow für die
|
||||
chamferten Rahmen (clip-path würde box-shadow wegschneiden) und box-shadow
|
||||
für den Stapel. Beide müssen mit. */
|
||||
.slide[data-shadow="aus"] .mock,
|
||||
.slide[data-shadow="aus"] .laptop,
|
||||
.slide[data-shadow="aus"] .hex-frame { filter: none; }
|
||||
.slide[data-shadow="aus"] .mock-inner,
|
||||
.slide[data-shadow="aus"] .sheet-mid,
|
||||
.slide[data-shadow="aus"] .sheet-back { box-shadow: none; }
|
||||
/* Screens are ~2000px wide landscape captures; showing them whole makes the
|
||||
UI text unreadable at this size, so each one is zoomed onto its telling
|
||||
region via the per-slide vars below. */
|
||||
@@ -539,29 +665,29 @@
|
||||
<div class="opts one" id="slides-nav"></div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label>Variante</label>
|
||||
<details class="group" data-grp="variante">
|
||||
<summary>Variante</summary>
|
||||
<div class="opts" id="variants">
|
||||
<button data-v="dunkel" aria-pressed="true"><span class="swatch" style="background:#30313D"></span>Dunkel</button>
|
||||
<button data-v="hell" aria-pressed="false"><span class="swatch" style="background:#FFFFFF"></span>Hell</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Layout (nur diese Slide)</label>
|
||||
<details class="group" data-grp="layout-nur-diese-slide">
|
||||
<summary>Layout (nur diese Slide)</summary>
|
||||
<div class="opts" id="layouts"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Tag optisch ausgerückt</label>
|
||||
<details class="group" data-grp="tag-optisch-ausger-ckt">
|
||||
<summary>Tag optisch ausgerückt</summary>
|
||||
<div class="opts" id="tagouts">
|
||||
<button data-o="an" aria-pressed="true">An</button>
|
||||
<button data-o="aus" aria-pressed="false">Aus</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Bearbeiten</label>
|
||||
<details class="group" data-grp="bearbeiten">
|
||||
<summary>Bearbeiten</summary>
|
||||
<div class="opts" id="editmodes" style="margin-bottom:8px">
|
||||
<button data-e="an" aria-pressed="false">An</button>
|
||||
<button data-e="aus" aria-pressed="true">Aus</button>
|
||||
@@ -575,31 +701,31 @@
|
||||
<button class="btn secondary" id="reset-one">Element zurück</button>
|
||||
</div>
|
||||
<button class="btn secondary" id="reset-all" style="width:100%;margin-top:8px">Alles auf Standard</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="std">
|
||||
<summary>Standardwerte</summary>
|
||||
<div id="std-list"></div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Ansicht</label>
|
||||
<details class="group" data-grp="ansicht">
|
||||
<summary>Ansicht</summary>
|
||||
<div class="opts" id="views">
|
||||
<button data-w="streifen" aria-pressed="true">Streifen</button>
|
||||
<button data-w="einzeln" aria-pressed="false">Einzeln</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Durchgehend (alle Slides)</label>
|
||||
<details class="group" data-grp="durchgehend-alle-slides">
|
||||
<summary>Durchgehend (alle Slides)</summary>
|
||||
<div class="opts" id="conts">
|
||||
<button data-c="an" aria-pressed="false">An</button>
|
||||
<button data-c="aus" aria-pressed="true">Aus</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Muster (nur diese Slide)</label>
|
||||
<details class="group" data-grp="muster-nur-diese-slide">
|
||||
<summary>Muster (nur diese Slide)</summary>
|
||||
<div class="opts three" id="bgs">
|
||||
<button data-b="formen" aria-pressed="true">Formen</button>
|
||||
<button data-b="formen-g" aria-pressed="false">Formen + G</button>
|
||||
@@ -609,32 +735,47 @@
|
||||
<button data-b="raster" aria-pressed="false">Raster</button>
|
||||
<button data-b="aus" aria-pressed="false">Aus</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Intensität</label>
|
||||
<details class="group" data-grp="intensit-t">
|
||||
<summary>Intensität</summary>
|
||||
<div class="opts" id="ints">
|
||||
<button data-i="dezent" aria-pressed="true">Dezent</button>
|
||||
<button data-i="kraeftig" aria-pressed="false">Kräftig</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Mockup-Perspektive</label>
|
||||
<details class="group" data-grp="bildrahmen">
|
||||
<summary>Bildrahmen</summary>
|
||||
<div class="opts three" id="frames">
|
||||
<button data-f="chamfer" aria-pressed="true">Chamfer</button>
|
||||
<button data-f="rund" aria-pressed="false">Rund</button>
|
||||
<button data-f="stapel" aria-pressed="false">Stapel</button>
|
||||
<button data-f="aus" aria-pressed="false">Aus</button>
|
||||
</div>
|
||||
<label style="display:block;margin:14px 0 9px">Schatten</label>
|
||||
<div class="opts" id="shadows">
|
||||
<button data-s="an" aria-pressed="true">An</button>
|
||||
<button data-s="aus" aria-pressed="false">Aus</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-grp="mockup-perspektive">
|
||||
<summary>Mockup-Perspektive</summary>
|
||||
<div class="opts" id="tilts">
|
||||
<button data-t="an" aria-pressed="true">Gekippt</button>
|
||||
<button data-t="aus" aria-pressed="false">Gerade</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Renderqualität</label>
|
||||
<details class="group" data-grp="renderqualit-t">
|
||||
<summary>Renderqualität</summary>
|
||||
<div class="opts three" id="quality">
|
||||
<button data-q="1" aria-pressed="false">1×</button>
|
||||
<button data-q="2" aria-pressed="true">2×</button>
|
||||
<button data-q="3" aria-pressed="false">3×</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="group">
|
||||
<label>Download</label>
|
||||
@@ -644,26 +785,40 @@
|
||||
<button id="dl-all" class="btn secondary" style="width:100%" disabled>Alle 5 Slides als ZIP</button>
|
||||
</div>
|
||||
|
||||
<div class="group" style="margin-bottom:0">
|
||||
<label>Tastatur</label>
|
||||
<details class="group" style="margin-bottom:0" data-grp="tastatur">
|
||||
<summary>Tastatur</summary>
|
||||
<div class="keys">
|
||||
<kbd>←</kbd><kbd>→</kbd> Slide · <kbd>V</kbd> Variante<br>
|
||||
<kbd>L</kbd> Layout · <kbd>M</kbd> Muster · <kbd>I</kbd> Intensität<br>
|
||||
<kbd>C</kbd> Durchgehend · <kbd>W</kbd> Ansicht · <kbd>G</kbd> Tag<br>
|
||||
<kbd>R</kbd> Bildrahmen · <kbd>S</kbd> Schatten<br>
|
||||
<kbd>T</kbd> Perspektive<br>
|
||||
<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>D</kbd> Slide laden · <kbd>A</kbd> alle als ZIP
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="status" id="status">Lade Schriften und Screens ...</div>
|
||||
</aside>
|
||||
|
||||
<main class="stage">
|
||||
<div class="strip-wrap" id="strip-wrap">
|
||||
<div class="strip" id="deck" data-view="streifen"></div>
|
||||
<main class="stage" id="stage">
|
||||
<div class="scroller" id="scroller">
|
||||
<div class="canvas">
|
||||
<div class="strip-wrap" id="strip-wrap">
|
||||
<div class="strip" id="deck" data-view="streifen"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hud">
|
||||
<p class="stage-meta" id="meta"></p>
|
||||
<div class="zoom">
|
||||
<button id="zoom-out" title="Auszoomen">−</button>
|
||||
<button class="level" id="zoom-fit" title="Einpassen">Fit</button>
|
||||
<button id="zoom-in" title="Einzoomen">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="stage-meta" id="meta">Vorschau 45 %, Export in voller Auflösung</p>
|
||||
</main>
|
||||
|
||||
<script src="./vendor/html-to-image.js"></script>
|
||||
@@ -707,7 +862,8 @@
|
||||
head: 'Du siehst auf einen Blick,<br><span class="a">wo jeder Auftrag steht.</span>',
|
||||
body: "Projekte, Kunden, Angebote, F\u00f6rderantr\u00e4ge. Kein Suchen, kein Nachfragen, kein Zettelchaos.",
|
||||
img: "./assets/screens/pipelines.png",
|
||||
imgScale: 1.28, imgX: "-2%", imgY: "-4%",
|
||||
mockAR: 1.8519, // 2000 x 1080
|
||||
imgScale: 1, imgX: "0%", imgY: "0%",
|
||||
},
|
||||
{
|
||||
key: "workflow-engine", type: "feature",
|
||||
@@ -716,7 +872,8 @@
|
||||
head: '<span class="a">Wenn-dann</span><br>statt Handarbeit.',
|
||||
body: "Ausl\u00f6ser w\u00e4hlen, Aktionen dranh\u00e4ngen, fertig. Anfrage rein, Aufgabe raus, Best\u00e4tigung an den Kunden, alles automatisch und ohne eine Zeile Code.",
|
||||
img: "./assets/screens/workflow.png",
|
||||
imgScale: 1.34, imgX: "-8%", imgY: "-22%",
|
||||
mockAR: 1.5038, // 2000 x 1330
|
||||
imgScale: 1, imgX: "0%", imgY: "0%",
|
||||
},
|
||||
{
|
||||
key: "chat", type: "feature",
|
||||
@@ -725,7 +882,9 @@
|
||||
head: 'Jedes Projekt bekommt<br>automatisch <span class="a">seinen Kanal.</span>',
|
||||
body: "Foto von der Baustelle rein, alle sehen\u2019s sofort und es landet direkt in den Projektdateien. Aus jeder Nachricht wird mit einem Klick eine Aufgabe.",
|
||||
img: "./assets/screens/chat.png",
|
||||
imgScale: 1.30, imgX: "-14%", imgY: "-8%",
|
||||
// 2000 x 1357. Etwas schmaler, sonst reicht die Hoehe unter dem Text nicht.
|
||||
mockW: 1070, mockAR: 1.4738,
|
||||
imgScale: 1, imgX: "0%", imgY: "0%",
|
||||
},
|
||||
{
|
||||
key: "form-builder", type: "feature",
|
||||
@@ -754,18 +913,35 @@
|
||||
"logo-full": { label: "Logo", size: 560, prop: "width" },
|
||||
"screen-logo": { label: "Logo im Screen", size: 430, prop: "width" },
|
||||
"laptop": { label: "Laptop", size: 960, prop: "width" },
|
||||
"mock": { label: "Bild", size: null },
|
||||
"hex-frame": { label: "Wabe", size: null },
|
||||
"mock": { label: "Bild", size: null, prop: "width" },
|
||||
"hex-frame": { label: "Wabe", size: 778, prop: "width" },
|
||||
};
|
||||
const EDIT_KEYS = Object.keys(STANDARD);
|
||||
|
||||
/* Der Standard des Bildrahmens hängt an der Slide: Der Form Builder hat ein
|
||||
Hochformat von 540, alle anderen 1090. Deshalb nicht aus der Tabelle,
|
||||
sondern aus der Slide selbst. */
|
||||
function stdSize(key, i) {
|
||||
if (key === "mock") return SLIDES[i].mockW || 1090;
|
||||
return (STANDARD[key] || {}).size;
|
||||
}
|
||||
|
||||
// Schriftgrößen in 2er-Schritten, Breiten in 2 % ihrer Ausgangsgröße:
|
||||
// 2 px auf ein 1090 px breites Bild würde man nie sehen.
|
||||
function stepFor(key, i) {
|
||||
const std = STANDARD[key];
|
||||
if (!std || !std.prop) return 0;
|
||||
if (std.prop === "font-size") return 2;
|
||||
return Math.max(4, Math.round(stdSize(key, i) * 0.02));
|
||||
}
|
||||
|
||||
let edits = SLIDES.map(() => ({}));
|
||||
|
||||
let picked = null; // { slide, key }
|
||||
let editMode = "aus";
|
||||
|
||||
let current = 0;
|
||||
let variant = "dunkel", tilt = "an", superSample = 2, continuous = "aus", view = "streifen", tagOut = "an";
|
||||
let variant = "dunkel", tilt = "an", superSample = 2, continuous = "aus", view = "streifen", tagOut = "an", frameStyle = "chamfer", shadow = "an";
|
||||
|
||||
/* ─── Projektstand ────────────────────────────────────────────────────────
|
||||
Alles, was am Projekt hängt, wird gespeichert: Einstellungen, Layouts,
|
||||
@@ -777,7 +953,7 @@
|
||||
function saveState() {
|
||||
try {
|
||||
localStorage.setItem(STORE, JSON.stringify({
|
||||
v: 1, variant, tilt, superSample, continuous, view, tagOut,
|
||||
v: 1, variant, tilt, superSample, continuous, view, tagOut, frameStyle, shadow,
|
||||
current, layoutState, bgState, edits,
|
||||
}));
|
||||
markSaved();
|
||||
@@ -805,6 +981,8 @@
|
||||
continuous = d.continuous || continuous;
|
||||
view = d.view || view;
|
||||
tagOut = d.tagOut || tagOut;
|
||||
frameStyle = d.frameStyle || frameStyle;
|
||||
shadow = d.shadow || shadow;
|
||||
current = Number.isInteger(d.current) ? d.current : 0;
|
||||
return true;
|
||||
}
|
||||
@@ -834,14 +1012,40 @@
|
||||
/* Der Streifen wird auf die verfügbare Breite skaliert, die Einzelansicht auf
|
||||
feste 45 %. Der Rahmen bekommt die skalierte Größe, damit die Seite nicht
|
||||
springt. */
|
||||
/* zoom === null bedeutet: automatisch einpassen. Sobald der Nutzer zoomt,
|
||||
hält der Wert, bis er auf "Fit" zurückgeht. */
|
||||
let zoom = null;
|
||||
|
||||
function stripWidth() { return view === "streifen" ? 1080 * SLIDES.length : 1080; }
|
||||
|
||||
function autoFit() {
|
||||
const scroller = document.getElementById("scroller");
|
||||
const availW = Math.max(320, scroller.clientWidth - 120);
|
||||
const availH = Math.max(240, scroller.clientHeight - 120);
|
||||
return Math.min(availW / stripWidth(), availH / 1350, 0.45);
|
||||
}
|
||||
|
||||
function fitStrip() {
|
||||
const wrap = document.getElementById("strip-wrap");
|
||||
const total = 1080 * SLIDES.length;
|
||||
const avail = Math.max(320, wrap.parentElement.clientWidth);
|
||||
const k = view === "streifen" ? Math.min(avail / total, 0.45) : 0.45;
|
||||
const k = zoom || autoFit();
|
||||
deck.style.transform = "scale(" + k + ")";
|
||||
wrap.style.width = Math.round((view === "streifen" ? total : 1080) * k) + "px";
|
||||
wrap.style.width = Math.round(stripWidth() * k) + "px";
|
||||
wrap.style.height = Math.round(1350 * k) + "px";
|
||||
const lvl = document.getElementById("zoom-fit");
|
||||
if (lvl) lvl.textContent = zoom ? Math.round(k * 100) + " %" : "Fit";
|
||||
}
|
||||
|
||||
function setZoom(k, anchor) {
|
||||
const scroller = document.getElementById("scroller");
|
||||
const before = zoom || autoFit();
|
||||
zoom = Math.min(2, Math.max(0.05, k));
|
||||
// Um den Mauszeiger zoomen, nicht um die linke obere Ecke.
|
||||
const ratio = zoom / before;
|
||||
const ax = anchor ? anchor.x : scroller.clientWidth / 2;
|
||||
const ay = anchor ? anchor.y : scroller.clientHeight / 2;
|
||||
fitStrip();
|
||||
scroller.scrollLeft = (scroller.scrollLeft + ax) * ratio - ax;
|
||||
scroller.scrollTop = (scroller.scrollTop + ay) * ratio - ay;
|
||||
}
|
||||
|
||||
const setStatus = (msg, kind) => {
|
||||
@@ -1030,7 +1234,14 @@
|
||||
if (sl.mockW) mock.style.setProperty("--mock-w", sl.mockW + "px");
|
||||
if (sl.mockAR) mock.style.setProperty("--mock-ar", sl.mockAR);
|
||||
if (sl.mockLeft) mock.style.setProperty("--mock-left", sl.mockLeft + "px");
|
||||
// Hinterstes Blatt bekommt den Screen der nächsten Feature-Slide.
|
||||
const withImg = SLIDES.filter(x => x.img);
|
||||
const other = withImg[(withImg.findIndex(x => x.img === sl.img) + 1) % withImg.length];
|
||||
mock.innerHTML =
|
||||
'<div class="sheet-back"><img alt="" src="' + assets[other.img] +
|
||||
'" style="transform:scale(' + other.imgScale + ') translate(' +
|
||||
other.imgX + ',' + other.imgY + ')"></div>' +
|
||||
'<div class="sheet-mid"></div>' +
|
||||
'<div class="mock-inner" style="--img-scale:' + sl.imgScale +
|
||||
';--img-x:' + sl.imgX + ';--img-y:' + sl.imgY + '">' +
|
||||
'<img class="laptop-shot" alt="" src="' + assets[sl.img] + '"></div>';
|
||||
@@ -1066,6 +1277,8 @@
|
||||
el.dataset.layout = layoutState[i];
|
||||
el.dataset.cont = continuous;
|
||||
el.dataset.tagout = tagOut;
|
||||
el.dataset.frame = frameStyle;
|
||||
el.dataset.shadow = shadow;
|
||||
el.dataset.editmode = editMode;
|
||||
el.style.setProperty("--slide-i", i);
|
||||
el.innerHTML = BG_HTML;
|
||||
@@ -1115,6 +1328,7 @@
|
||||
}
|
||||
|
||||
function applyAll() {
|
||||
deck.style.background = variant === "hell" ? "#FFFFFF" : "#30313D";
|
||||
deck.querySelectorAll(".slide").forEach((el, i) => {
|
||||
el.dataset.variant = variant;
|
||||
el.dataset.tilt = tilt;
|
||||
@@ -1123,6 +1337,8 @@
|
||||
el.dataset.layout = layoutState[i];
|
||||
el.dataset.cont = continuous;
|
||||
el.dataset.tagout = tagOut;
|
||||
el.dataset.frame = frameStyle;
|
||||
el.dataset.shadow = shadow;
|
||||
if (SLIDES[i].type === "title") rebuildPage(i);
|
||||
el.dataset.editmode = editMode;
|
||||
});
|
||||
@@ -1431,12 +1647,13 @@
|
||||
saveState();
|
||||
}
|
||||
|
||||
function resize(delta) {
|
||||
function resize(dir) {
|
||||
if (!picked) return;
|
||||
const std = STANDARD[picked.key];
|
||||
if (!std || !std.prop) return;
|
||||
const base = stdSize(picked.key, picked.slide);
|
||||
const set = (edits[picked.slide][picked.key] = edits[picked.slide][picked.key] || {});
|
||||
set.size = Math.max(8, (set.size || std.size) + delta);
|
||||
set.size = Math.max(8, (set.size || base) + dir * stepFor(picked.key, picked.slide));
|
||||
applyEditsAll();
|
||||
pick(picked.slide, picked.key);
|
||||
saveState();
|
||||
@@ -1465,13 +1682,30 @@
|
||||
if (!picked) { box.innerHTML = '<span class="dim">Nichts ausgewählt</span>'; return; }
|
||||
const std = STANDARD[picked.key] || {};
|
||||
const e = (edits[picked.slide] || {})[picked.key] || {};
|
||||
const val = e.size || std.size;
|
||||
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 " + std.size + ")" : "") + "<br>" : "") +
|
||||
(std.prop ? val + " " + unit + (e.size ? " (Standard " + base + ")" : "") + "<br>" : "") +
|
||||
"Versatz " + (e.dx || 0) + " / " + (e.dy || 0) + " px";
|
||||
}
|
||||
/* Welche Kategorien offen sind, ist Bedienvorliebe und gehört nicht in den
|
||||
Projektstand: gilt für beide Projekte gleichermaßen. */
|
||||
(function rememberGroups() {
|
||||
const KEY = "hivegrid.ui.gruppen";
|
||||
let open = [];
|
||||
try { open = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (_) {}
|
||||
document.querySelectorAll("details.group[data-grp]").forEach(d => {
|
||||
if (open.includes(d.dataset.grp)) d.open = true;
|
||||
d.addEventListener("toggle", () => {
|
||||
const now = [...document.querySelectorAll("details.group[open][data-grp]")]
|
||||
.map(x => x.dataset.grp);
|
||||
try { localStorage.setItem(KEY, JSON.stringify(now)); } catch (_) {}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
document.getElementById("forget").addEventListener("click", forgetProject);
|
||||
document.getElementById("editmodes").addEventListener("click", e => {
|
||||
const b = e.target.closest("button");
|
||||
@@ -1485,8 +1719,8 @@
|
||||
syncEditPanel();
|
||||
}
|
||||
});
|
||||
document.getElementById("sz-plus").addEventListener("click", () => resize(2));
|
||||
document.getElementById("sz-minus").addEventListener("click", () => resize(-2));
|
||||
document.getElementById("sz-plus").addEventListener("click", () => resize(1));
|
||||
document.getElementById("sz-minus").addEventListener("click", () => resize(-1));
|
||||
document.getElementById("reset-one").addEventListener("click", resetOne);
|
||||
document.getElementById("reset-all").addEventListener("click", resetAll);
|
||||
|
||||
@@ -1520,10 +1754,56 @@
|
||||
});
|
||||
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); show(current); saveState(); }
|
||||
if (b) { view = b.dataset.w; deck.dataset.view = view; press("views", "w", view); zoom = null; show(current); saveState(); }
|
||||
});
|
||||
window.addEventListener("resize", fitStrip);
|
||||
|
||||
/* Strg/Cmd + Rad zoomt, blankes Rad scrollt. Trackpad-Pinch schickt
|
||||
ctrlKey mit, verhält sich also automatisch richtig. */
|
||||
document.getElementById("scroller").addEventListener("wheel", e => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
const r = e.currentTarget.getBoundingClientRect();
|
||||
setZoom((zoom || autoFit()) * (e.deltaY < 0 ? 1.12 : 1 / 1.12),
|
||||
{ x: e.clientX - r.left, y: e.clientY - r.top });
|
||||
}, { passive: false });
|
||||
|
||||
/* Panning als verlässlicher Weg, unabhängig von Trackpad-Gesten:
|
||||
Leertaste gedrückt halten und ziehen, oder mittlere Maustaste. */
|
||||
(function panning() {
|
||||
const sc = document.getElementById("scroller");
|
||||
let space = false, active = null;
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.code !== "Space" || e.target.matches("input, textarea")) return;
|
||||
e.preventDefault();
|
||||
if (!space) { space = true; sc.classList.add("panready"); }
|
||||
});
|
||||
document.addEventListener("keyup", e => {
|
||||
if (e.code !== "Space") return;
|
||||
space = false; sc.classList.remove("panready");
|
||||
});
|
||||
|
||||
sc.addEventListener("mousedown", e => {
|
||||
if (!space && e.button !== 1) return;
|
||||
e.preventDefault();
|
||||
active = { x: e.clientX, y: e.clientY, l: sc.scrollLeft, t: sc.scrollTop };
|
||||
sc.classList.add("panning");
|
||||
});
|
||||
document.addEventListener("mousemove", e => {
|
||||
if (!active) return;
|
||||
sc.scrollLeft = active.l - (e.clientX - active.x);
|
||||
sc.scrollTop = active.t - (e.clientY - active.y);
|
||||
});
|
||||
document.addEventListener("mouseup", () => {
|
||||
active = null; sc.classList.remove("panning");
|
||||
});
|
||||
})();
|
||||
|
||||
document.getElementById("zoom-in").addEventListener("click", () => setZoom((zoom || autoFit()) * 1.25));
|
||||
document.getElementById("zoom-out").addEventListener("click", () => setZoom((zoom || autoFit()) / 1.25));
|
||||
document.getElementById("zoom-fit").addEventListener("click", () => { zoom = null; fitStrip(); });
|
||||
|
||||
nav.addEventListener("click", e => {
|
||||
const b = e.target.closest("button"); if (b) show(Number(b.dataset.n));
|
||||
});
|
||||
@@ -1547,6 +1827,14 @@
|
||||
const b = e.target.closest("button");
|
||||
if (b) { bgState[current].int = b.dataset.i; syncBgButtons(); 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(); }
|
||||
});
|
||||
document.getElementById("shadows").addEventListener("click", e => {
|
||||
const b = e.target.closest("button");
|
||||
if (b) { shadow = b.dataset.s; press("shadows", "s", shadow); applyAll(); saveState(); }
|
||||
});
|
||||
document.getElementById("tilts").addEventListener("click", e => {
|
||||
const b = e.target.closest("button");
|
||||
if (b) { tilt = b.dataset.t; press("tilts", "t", tilt); applyAll(); saveState(); }
|
||||
@@ -1593,8 +1881,18 @@
|
||||
syncBgButtons(); applyAll();
|
||||
}
|
||||
else if (k === "t") { tilt = tilt === "an" ? "aus" : "an"; press("tilts", "t", tilt); applyAll(); saveState(); }
|
||||
else if (k === "w") { view = view === "streifen" ? "einzeln" : "streifen"; deck.dataset.view = view; press("views", "w", view); show(current); }
|
||||
else if (e.key === "+" || e.key === "=") { setZoom((zoom || autoFit()) * 1.25); }
|
||||
else if (e.key === "-") { setZoom((zoom || autoFit()) / 1.25); }
|
||||
else if (e.key === "0") { zoom = null; fitStrip(); }
|
||||
else if (k === "w") { view = view === "streifen" ? "einzeln" : "streifen"; deck.dataset.view = view; press("views", "w", view); zoom = null; show(current); }
|
||||
else if (k === "e") { document.querySelector('#editmodes button[data-e="' + (editMode === "an" ? "aus" : "an") + '"]').click(); }
|
||||
else if (k === "r") {
|
||||
const cyc = ["chamfer", "rund", "stapel", "aus"];
|
||||
frameStyle = cyc[(cyc.indexOf(frameStyle) + 1) % cyc.length];
|
||||
press("frames", "f", frameStyle);
|
||||
press("shadows", "s", shadow); applyAll(); saveState();
|
||||
}
|
||||
else if (k === "s") { shadow = shadow === "an" ? "aus" : "an"; press("shadows", "s", shadow); applyAll(); saveState(); }
|
||||
else if (k === "g") { tagOut = tagOut === "an" ? "aus" : "an"; press("tagouts", "o", tagOut); applyAll(); saveState(); }
|
||||
else if (k === "c") { continuous = continuous === "an" ? "aus" : "an"; press("conts", "c", continuous); applyAll(); saveState(); }
|
||||
else if (k === "d") guard(downloadOne)();
|
||||
@@ -1616,6 +1914,8 @@
|
||||
press("quality", "q", String(superSample));
|
||||
press("conts", "c", continuous);
|
||||
press("tagouts", "o", tagOut);
|
||||
press("frames", "f", frameStyle);
|
||||
press("shadows", "s", shadow);
|
||||
press("views", "w", view);
|
||||
show(current);
|
||||
if (restored) setStatus("Projektstand geladen", "ok");
|
||||
|
||||
Reference in new issue
Block a user