Post 6: fuenfte Slide fuer den mobilen Chat, Tiefe ueber den Stapel
Zwei Nachbesserungen an Post 6: eine Slide mehr und Zeichnungen, die nicht mehr flach aussehen. DIE FUENFTE SLIDE KOMMT VON DER WEBSITE Die App gibt es noch nicht, und die Website sagt das bereits. In components/sections/ChatDemoSection.tsx steht unter der Fahne comingSoon genau dieser Zustand: ein Handy-Mockup, verwischt, und darueber ein Absperrband aus dem Bau -- gedreht um -13 Grad, in accent-yellow F1B729 mit Kanten in 171106, darauf immer wieder Schutzhelm und BALD VERFUEGBAR. Ein Zwischenstand hatte hier einen selbst erfundenen gelben Aufkleber "In Arbeit". Das war eine zweite Bildsprache neben einer vorhandenen. Uebernommen ist jetzt die der Website, samt Winkel, Farben und Wortlaut. Auch der Inhalt des Schirms kommt von dort und nicht aus dem Pitch-Paket: Heizungsprojekt Schulz, "4 Mitglieder · 2 online", die vier Nachrichten aus seedMessages, die Blasenfarben 0D6E6E fuer eigene und FFFFFF mit Rand E9E7E5 fuer fremde, der warme Grund hsl(30 17% 95.5%). Ueberschrift und Fliesstext ebenso: "Der Chat fuer unterwegs wird gerade gebaut." Das Geraet selbst ist eine bewusste Abweichung. Die Website zeichnet nur einen hellen Umriss, border 4px D1D5DB; auf einer Slide, die selbst hell ist, faellt der in sich zusammen und sieht nach Platzhalter aus. Hier steht ein richtiges Geraet: dunkler Korpus, heller Grat, Schirm eingelassen, Insel und Tasten. Die Abweichung betrifft den Rahmen, nicht den Inhalt. Verwischt wird schwaecher als dort -- auf der Website liegt das Handy gross neben Fliesstext und darf unlesbar sein, hier ist es klein, und bei voller Staerke blieb Brei. DIE TIEFE Zwei Anlaeufe waren falsch. Der erste legte graue Flaechen hinter die Elemente: das nimmt den Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den Punktgrund, der dort nicht hingehoert. Wieder raus, die Zeichnungen sind wieder durchsichtig. Der zweite waren bessere Schatten allein. Richtig, aber zu wenig. Die Schatten des Produkts sind in ui/theme.css gestaffelt und nicht einlagig: --shadow-md 0 4px 16px -2px + 0 2px 4px -1px --shadow-lg 0 12px 32px -4px + 0 4px 8px -2px Zwei Lagen, weil eine allein nicht beides kann: die enge dunkle Kante setzt das Element auf die Flaeche, die weite helle hebt es davon ab. In SVG zwei feDropShadow im selben Filter. Vorher stand dort einer mit dy 7. Gebracht hat es der Stapel. post1.html hat ihn als Bildrahmen im CSS, fuer Screenshots; hier ist er in SVG nachgebaut, mit denselben Werten: .sheet-mid rotate(-2.4deg) translate(-2.2%, 3%) scale(.975) .sheet-back rotate( 3.2deg) translate( 3%, 5.5%) scale(.95) Er gibt Tiefe, die ein Schatten allein nicht schafft, und sagt, dass hinter der einen Ansicht weitere liegen. Die hinteren Blaetter sind leer -- Andeutung, keine Kopie. Auf Slide 1 und 2, wo ein Bildschirm zu sehen ist; nicht auf Slide 3, wo ein Ausschnitt einer Unterhaltung steht, und nicht auf Slide 4, die eine Ueberlagerung ist. AUS DER QUELLE NACHGEHOLT Beim gruendlichen Durchgang durch GeneralTeamChatPage.tsx sind drei Dinge aufgefallen, die vorher fehlten oder falsch waren: - Die Datumsmarke "Heute · 30.07.2026" ueber dem ersten Beitrag und die Zeile "Maike Berger schreibt gerade ..." ueber dem Eingabefeld. Beides gibt der Unterhaltung Struktur, ohne eine Flaeche zu behaupten. - Der offene Kanal ist grau hinterlegt mit Rand und shadow-sm, nicht violett. - Die Blase muss so breit sein, wie ihr Anhang es braucht. Vorher richtete sie sich nur nach dem Fliesstext, und "Materialliste pruefen · faellig 31.07.2026" wurde abgeschnitten -- an einer Stelle, an der der Titel die Aussage traegt. ZWEI FALLEN Gedrehte Blaetter ragen aus dem Kasten. Das hinterste steht um 3,2 Grad gedreht und um 3 und 5,5 Prozent verschoben; seine Ecken lagen bis zu 24 px ausserhalb der 92-px-Schutzzone. Der Ueberstand wird jetzt gerechnet: stapelRand() schickt die vier Ecken durch dieselbe Kette wie das Blatt. getBBox() kennt keine Beschneidung. Das Absperrband war erst auf die 175 % der Website gezogen und am Geraet abgeschnitten -- im Bild richtig, in der Messung ein Ueberstand von 146 px, den es nicht gab. Es ist jetzt nur so lang, wie es bei -13 Grad quer ueber das Geraet sein muss. GEPRUEFT Raender 93 / 227 / 110 / 140 / 350, Luft 116 / 79 / 190 / 141 / 77 px, alle Ueberschriften innerhalb der Schutzzone, 214 CSS-Regeln, Konsole ohne Fehler, fuenf PNGs aus dem echten Ausgabeweg bei 2160 x 2700. Downloads blieb leer, der Projektstand unveraendert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7723e5bf5a
commit
ce87a7fb57
4 files changed
+511
-37
No files matched your search
@@ -8,8 +8,8 @@ Hell, Theme `modern`, Punktgrund, Text zentriert — dieselbe Anlage wie Post 4
|
||||
und 5. Datei `post6.html`, Stand in `state/post6.json`, erreichbar über das
|
||||
Projektmenü jeder Seite und über `uebersicht.html`.
|
||||
|
||||
**Vier Slides.** Fünf waren erlaubt; der fünfte Punkt wäre DSGVO und deutsche
|
||||
Server gewesen, und dafür gibt es keinen Bildschirm. Er steht in der
|
||||
**Fünf Slides.** DSGVO und deutsche Server hätten einen sechsten Punkt
|
||||
ergeben, dafür gibt es aber keinen Bildschirm. Der steht in der
|
||||
Bildunterschrift, wo eine Aussage ohne Bild hingehört.
|
||||
|
||||
---
|
||||
@@ -22,6 +22,7 @@ Bildunterschrift, wo eine Aussage ohne Bild hingehört.
|
||||
| 2 | Die Kanäle | Ein Kanal **pro Projekt.** | Die Chatliste groß: Projektkanäle, Direktnachrichten, Gruppe |
|
||||
| 3 | Die Aufgabe | Foto rein, **Aufgabe raus.** | Baustellenfotos, die Aufgabe daraus, die Bestätigung im Projekt |
|
||||
| 4 | Das Pluszeichen | Nicht nur **Text.** | Das Menü hinter dem Plus im Eingabefeld |
|
||||
| 5 | Bald verfügbar | Der Chat für unterwegs **wird gerade gebaut.** | Das Handy-Mockup der Website, verwischt, mit Absperrband |
|
||||
|
||||
**Slide 3 ist der Kern.** Der Redaktionsplan verlangt „Foto rein, Aufgabe
|
||||
raus", und genau das liegt im Werkzeug als Abfolge vor: Tobias schickt die
|
||||
@@ -110,14 +111,14 @@ Beitrag zu setzen, der von der eigenen Baustelle spricht, wäre eine Behauptung
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Slides | 4 |
|
||||
| Slides | 5 |
|
||||
| Format | 1080 × 1350, Ausgabe zweifach: 2160 × 2700 |
|
||||
| Luft zwischen Text und Zeichnung | 102 / 61 / 254 / 141 px (Grenze 40) |
|
||||
| Luft zwischen Text und Zeichnung | 116 / 79 / 190 / 141 / 77 px (Grenze 40) |
|
||||
| Zeichnung ragt über die Seite | nein, alle innerhalb der 92-px-Schutzzone |
|
||||
| Breiteste Überschriftzeile | 818 px von 896 |
|
||||
| Stylesheet vollständig | 214 Regeln, `[data-edit]` vorhanden |
|
||||
| Konsole | keine Fehler |
|
||||
| Ausgabeweg | geprüft, `01-chat-hell.png` bis `04-menue-hell.png` |
|
||||
| Ausgabeweg | geprüft, `01-chat-hell.png` bis `05-handy-hell.png` |
|
||||
|
||||
Geprüft wird mit den Hilfsskripten aus dem Sitzungsordner: `pruef.mjs` misst
|
||||
über `getBBox()` die tatsächlich gezeichnete Fläche je Slide und die Breite
|
||||
@@ -127,7 +128,94 @@ Ausgabeweg und fängt `/save` ab.
|
||||
|
||||
---
|
||||
|
||||
## 6. Drei Fallen aus diesem Bau
|
||||
## 5a. Slide 5 kommt von der Website, nicht von mir
|
||||
|
||||
**Das Gerät ist eine bewusste Abweichung.** Die Website zeichnet nur einen
|
||||
hellen Umriss (`border: 4px solid #D1D5DB`). Auf einer Slide, die selbst hell
|
||||
ist, fällt der in sich zusammen und sieht nach Platzhalter aus. Hier steht
|
||||
deshalb ein richtiges Gerät: dunkler Korpus, heller Grat, Schirm eingelassen,
|
||||
Tasten an der Seite. Die Abweichung betrifft den Rahmen, nicht den Inhalt. Der
|
||||
Schirm wird auch schwächer verwischt als dort: auf der Website liegt das Handy
|
||||
groß neben Fließtext und darf unlesbar sein, hier ist es klein, und bei voller
|
||||
Stärke blieb nur Brei.
|
||||
|
||||
**Die App gibt es noch nicht, und die Website sagt das bereits.** In
|
||||
`components/sections/ChatDemoSection.tsx` steht unter der Fahne `comingSoon`
|
||||
genau dieser Zustand: ein Handy-Mockup, das mit `blur(3px) saturate(0.92)`
|
||||
verwischt wird, und darüber ein **Absperrband** aus dem Bau, gedreht um
|
||||
**-13°**, in `--accent-yellow` `#F1B729` mit Kanten in `#171106`, darauf immer
|
||||
wieder Schutzhelm und **BALD VERFÜGBAR**.
|
||||
|
||||
Ein Zwischenstand hatte hier einen selbst erfundenen gelben Aufkleber
|
||||
„In Arbeit". Das war eine eigene Bildsprache neben einer vorhandenen.
|
||||
Übernommen ist jetzt die der Website, samt Winkel, Farben und Wortlaut.
|
||||
|
||||
Auch der Inhalt des Schirms kommt von dort und **nicht** aus dem Pitch-Paket:
|
||||
Heizungsprojekt Schulz, „4 Mitglieder · 2 online", die vier Nachrichten aus
|
||||
`seedMessages`, die Blasenfarben `#0D6E6E` für eigene und `#FFFFFF` mit Rand
|
||||
`#E9E7E5` für fremde, der warme Grund `hsl(30 17% 95.5%)`. Auf der Website
|
||||
steht dieses Handy für den mobilen Chat, also zeigt es hier dasselbe.
|
||||
|
||||
Überschrift und Fließtext sind ebenfalls die der Website: „Der Chat für
|
||||
unterwegs wird gerade gebaut." und „Die mobile App legen wir gerade auf die
|
||||
Baustelle. Am Desktop bist du schon voll dabei, die Version fürs Handy folgt
|
||||
in Kürze." Der Em-Dash der Vorlage ist durch ein Komma ersetzt, `rules.md` §4.
|
||||
|
||||
---
|
||||
|
||||
## 5b. Woher die Tiefe kommt
|
||||
|
||||
Zwei Anläufe waren falsch, bevor es saß.
|
||||
|
||||
**Erster Versuch: graue Flächen hinter die Elemente.** Das nimmt den
|
||||
Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den Punktgrund,
|
||||
der dort nicht hingehört. Wieder raus.
|
||||
|
||||
**Zweiter Versuch: nur bessere Schatten.** Richtig, aber zu wenig. Die
|
||||
Schatten des Produkts sind in `ui/theme.css` **gestaffelt** und nicht
|
||||
einlagig, und das war tatsächlich ein Fehler im ersten Nachbau:
|
||||
|
||||
```
|
||||
--shadow-sm 0 1px 2px rgb(0 0 0 / .08)
|
||||
--shadow-md 0 4px 16px -2px + 0 2px 4px -1px
|
||||
--shadow-lg 0 12px 32px -4px + 0 4px 8px -2px
|
||||
```
|
||||
|
||||
Zwei Lagen, weil eine allein nicht beides kann: die enge dunkle Kante setzt
|
||||
das Element auf die Fläche, die weite helle hebt es davon ab. In SVG zwei
|
||||
`feDropShadow` im selben Filter. Vorher stand dort einer mit `dy 7` und
|
||||
`stdDeviation 10`.
|
||||
|
||||
**Was es gebracht hat: der Stapel.** `post1.html` hat ihn als Bildrahmen
|
||||
`stapel` im CSS, für Screenshots. Hier ist er in SVG nachgebaut, mit genau
|
||||
denselben Werten, damit die Reihe zusammenpasst:
|
||||
|
||||
```
|
||||
.sheet-mid rotate(-2.4deg) translate(-2.2%, 3%) scale(.975)
|
||||
.sheet-back rotate( 3.2deg) translate( 3%, 5.5%) scale(.95)
|
||||
```
|
||||
|
||||
Er trägt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein nicht schafft,
|
||||
und er sagt, dass hinter der einen Ansicht weitere liegen. Die hinteren
|
||||
Blätter sind deshalb **leer** — Andeutung, keine Kopie.
|
||||
|
||||
Auf Slide 1 und 2 liegt er, weil dort ein Bildschirm zu sehen ist. Auf Slide 3
|
||||
nicht: ein Ausschnitt aus einer Unterhaltung ist kein Blatt, dort tragen die
|
||||
Blasen ihre eigene Erhebung. Slide 4 ist eine Überlagerung und trägt
|
||||
`--shadow-overlay`.
|
||||
|
||||
Dazu zwei Dinge aus der Quelle, die der Unterhaltung Struktur geben, ohne eine
|
||||
Fläche zu behaupten: die Datumsmarke **Heute · 30.07.2026** über dem ersten
|
||||
Beitrag und die Zeile **„Maike Berger schreibt gerade …"** über dem
|
||||
Eingabefeld. Beides steht so in `GeneralTeamChatPage.tsx`.
|
||||
|
||||
Nebenbei richtiggestellt: **der offene Kanal ist grau hinterlegt, nicht
|
||||
violett.** Im Original steht dort `bg-surface-neutral-primary` mit Rand und
|
||||
`shadow-sm`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Fünf Fallen aus diesem Bau
|
||||
|
||||
**Die Listenzeile hat drei Zeilen, nicht zwei.** Der erste Anlauf setzte Titel
|
||||
und Vorschau untereinander und schob Nadel und Ungelesen-Punkt rechts neben
|
||||
@@ -150,6 +238,21 @@ Nachgemessen sind es 818 px von 896 erlaubten: sie passt. Das Wort ist wieder
|
||||
drin. Dafür gibt es jetzt `breite.mjs`, das eine Formulierung misst, bevor sie
|
||||
in die Datei kommt.
|
||||
|
||||
**Gedrehte Blätter ragen aus dem Kasten.** Der Stapel dreht das hinterste
|
||||
Blatt um 3,2° und verschiebt es um 3 % und 5,5 % der eigenen Größe. Die Ecken
|
||||
stehen damit über das vordere Blatt hinaus, und die Zeichnung lag bis zu 24 px
|
||||
außerhalb der 92-px-Schutzzone. Der Überstand wird jetzt gerechnet statt
|
||||
geschätzt: `stapelRand()` schickt die vier Ecken durch dieselbe Kette wie das
|
||||
Blatt und nimmt den größten Wert. `stapelKasten()` macht den Kasten
|
||||
entsprechend größer.
|
||||
|
||||
**`getBBox()` kennt keine Beschneidung.** Das Absperrband war zuerst auf die
|
||||
175 % der Website gezogen und per `clip-path` am Gerät abgeschnitten. Im Bild
|
||||
stimmte das, die Randmessung meldete aber einen Überstand von 146 px, den es
|
||||
gar nicht gab: `getBBox()` liefert die Geometrie, nicht das Sichtbare. Das
|
||||
Band ist jetzt nur so lang, wie es bei -13° quer über das Gerät sein muss,
|
||||
also `B / cos(13°)` plus Zugabe. Messung und Bild sagen wieder dasselbe.
|
||||
|
||||
**Das Prüfskript nahm eine leere Slide auf.** `schuss.mjs` stellte für eine
|
||||
einzelne Slide ein Fenster von 1400 px. Im durchgehenden Blatt liegt Seite 3
|
||||
aber bei x = 2160, und `captureBeyondViewport` fing das nicht zuverlässig ab.
|
||||
|
||||
@@ -26,7 +26,7 @@ Downloads landen direkt in `~/Downloads`, ohne Browser-Dialog.
|
||||
| Post 3 · Verbinden statt ersetzen | `post3.html` | Einzelbild, Theme `modern`, erscheint 17.08. | |
|
||||
| Post 4 · Workflow-Engine | `post4.html` | Carousel, 4 Slides, Theme `modern`, erscheint 19.08. | [`POST-4.md`](./POST-4.md) |
|
||||
| Post 5 · Projektmanagement | `post5.html` | Carousel, 5 Slides, Theme `modern`, erscheint 21.08. | [`POST-5.md`](./POST-5.md) |
|
||||
| Post 6 · Team-Chat | `post6.html` | Carousel, 4 Slides, Theme `modern`, erscheint 26.08. | [`POST-6.md`](./POST-6.md) |
|
||||
| Post 6 · Team-Chat | `post6.html` | Carousel, 5 Slides, Theme `modern`, erscheint 26.08. | [`POST-6.md`](./POST-6.md) |
|
||||
|
||||
**Alle Streifen nebeneinander** zeigt `uebersicht.html`, erreichbar über
|
||||
*Projekt → Alle Projekte im Streifen*. Die Seite baut die Slides nicht selbst
|
||||
@@ -224,6 +224,7 @@ Baustellenfotos.
|
||||
| 2 | die Chatliste groß: Projektkanäle, Direktnachrichten, Gruppe |
|
||||
| 3 | Fotos, die Aufgabe daraus, die Bestätigung im Projekt |
|
||||
| 4 | das Menü hinter dem Plus im Eingabefeld |
|
||||
| 5 | das Handy-Mockup der Website, verwischt, mit Absperrband |
|
||||
|
||||
**Slide 3 ist der Kern**, und sie ist keine Konstruktion: im Werkzeug folgen
|
||||
die drei Nachrichten genau so aufeinander. Tobias schickt die Baustellenfotos,
|
||||
@@ -231,9 +232,26 @@ darauf eine Nachricht mit angehängter Aufgabe, darauf die Systemzeile
|
||||
„Aufgabe „Materialliste prüfen" im Projekt erstellt". Die letzte Zeile macht
|
||||
den Unterschied: die Aufgabe liegt danach im Projekt, nicht nur im Verlauf.
|
||||
|
||||
**Vier Slides, nicht fünf.** Der fünfte Punkt wäre DSGVO und deutsche Server
|
||||
gewesen. Dafür gibt es keinen Bildschirm, und eine Behauptung ohne Bild gehört
|
||||
in die Bildunterschrift.
|
||||
**Slide 5 kommt von der Website, nicht aus dem Pitch-Paket.** Die App gibt es
|
||||
noch nicht, und `ChatDemoSection.tsx` hat den Zustand bereits: ein Handy-Mockup
|
||||
mit `blur(3px)` und darüber ein **Absperrband** aus dem Bau, -13° gedreht, in
|
||||
`#F1B729` mit Kanten in `#171106`, darauf Schutzhelm und „BALD VERFÜGBAR".
|
||||
Auch Inhalt, Blasenfarben, Überschrift und Fließtext sind die der Website. Ein
|
||||
Zwischenstand hatte hier einen selbst erfundenen Aufkleber; das war eine eigene
|
||||
Bildsprache neben einer vorhandenen.
|
||||
|
||||
**Die Tiefe kommt aus dem Stapel und gestaffelten Schatten, nicht aus
|
||||
Flächen.** Ein Zwischenstand legte graue Flächen hinter die Elemente; das nimmt
|
||||
den Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den
|
||||
Punktgrund. Stattdessen zweierlei: die echten Schatten aus `ui/theme.css`, die
|
||||
dort **gestaffelt** sind (enge dunkle Kante plus weite helle, in SVG zwei
|
||||
`feDropShadow` im selben Filter), und der **Stapel** aus `post1.html`, in SVG
|
||||
nachgebaut mit denselben Winkeln, die dort im CSS stehen. Die hinteren Blätter
|
||||
sind leer: Andeutung, keine Kopie. Auf Slide 1 und 2, wo ein Bildschirm zu
|
||||
sehen ist; nicht auf Slide 3, wo ein Ausschnitt einer Unterhaltung steht.
|
||||
|
||||
Dazu die Datumsmarke und die Zeile „Maike Berger schreibt gerade …" aus der
|
||||
Quelle, die der Unterhaltung Struktur geben, ohne eine Fläche zu behaupten.
|
||||
|
||||
**Die Bildergalerie ist eine Anhangkarte, kein Bild.** Im Werkzeug lädt
|
||||
`GalleryPreview` zwei Fotos von Unsplash. Fremde Fotos in einen Beitrag zu
|
||||
|
||||
+366
-27
@@ -1305,7 +1305,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
eyebrow: "Team-Chat",
|
||||
head: 'Büro und Baustelle,<br><span class="a">endlich im selben Chat.</span>',
|
||||
body: "",
|
||||
vis: "chat", visW: 896, visLeft: 92, visBottom: 120,
|
||||
vis: "chat", visW: 948, visLeft: 66, visBottom: 120,
|
||||
},
|
||||
{
|
||||
/* Der Fliesstext ist das Merkmal "Themen-Kanäle" von der Website,
|
||||
@@ -1316,7 +1316,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
eyebrow: "Die Kanäle",
|
||||
head: 'Ein Kanal<br><span class="a">pro Projekt.</span>',
|
||||
body: "Projektkanäle, Direktnachrichten, Gruppen. Jeder Kanal hängt an seiner Projektnummer.",
|
||||
vis: "kanaele", visW: 620, visLeft: 230, visBottom: 120,
|
||||
vis: "kanaele", visW: 672, visLeft: 204, visBottom: 120,
|
||||
},
|
||||
{
|
||||
/* Der Satz aus dem Redaktionsplan: "Foto rein, Aufgabe raus." Die
|
||||
@@ -1337,6 +1337,22 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
body: "Dokument, Fotos, Sprachnachricht, Kontakt. Und wenn daraus etwas werden soll: Termin oder Aufgabe.",
|
||||
vis: "menue", visW: 800, visLeft: 140, visBottom: 120,
|
||||
},
|
||||
{
|
||||
/* Ein Ausblick, kein Beleg. Die App gibt es noch nicht -- deshalb steht
|
||||
die Marke "In Arbeit" ueber dem Geraet, und der Fliesstext sagt es
|
||||
geradeheraus. Auf dem Schirm ist nichts Neues: dieselben zwei
|
||||
Nachrichten wie auf Slide 1, nur schmaler gesetzt.
|
||||
|
||||
"Im Buero am Rechner, auf der Baustelle am Handy" steht als Merkmal
|
||||
"Desktop & Mobile" auf der Website. Der Satz beschreibt das Ziel,
|
||||
nicht den heutigen Stand -- deshalb ist er hier nicht zitiert. */
|
||||
key: "handy", type: "feature",
|
||||
layouts: [{ id: "standard", label: "Standard" }],
|
||||
eyebrow: "Bald verfügbar",
|
||||
head: 'Der Chat für unterwegs<br><span class="a">wird gerade gebaut.</span>',
|
||||
body: "Die mobile App legen wir gerade auf die Baustelle. Am Desktop bist du schon voll dabei, die Version fürs Handy folgt in Kürze.",
|
||||
vis: "handy", visW: 358, visLeft: 361, visBottom: 120,
|
||||
},
|
||||
];
|
||||
|
||||
/* Vorlage für eine selbst angelegte Slide. Sie startet bewusst mit Text
|
||||
@@ -1482,10 +1498,45 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
mit beschnitten (lib/chamfer.ts der Website). Die doppelte id in
|
||||
mehreren Blaettern stoert nicht: alle Filter sind gleich, und url(#..)
|
||||
nimmt den ersten. */
|
||||
/* Die Schatten des Produkts, aufgeloest aus ui/theme.css. Sie sind
|
||||
gestaffelt und nicht einlagig -- genau daran hing es, dass die
|
||||
Zeichnungen flach aussahen:
|
||||
|
||||
--shadow-sm 0 1px 2px rgb(0 0 0 / .08)
|
||||
--shadow-md 0 4px 16px -2px + 0 2px 4px -1px
|
||||
--shadow-lg 0 12px 32px -4px + 0 4px 8px -2px
|
||||
|
||||
Zwei Lagen, weil eine allein nicht beides kann: die enge dunkle Kante
|
||||
setzt das Element auf die Flaeche, die weite helle hebt es davon ab. In
|
||||
SVG heisst das zwei feDropShadow hintereinander im selben Filter.
|
||||
|
||||
Der negative Spread der CSS-Fassung (-2px, -4px) laesst sich in SVG nicht
|
||||
nachbilden; er wird ueber eine etwas kleinere Streuung angenaehert. */
|
||||
/* Wie wfSvg, aber mit Rand: der Inhalt wird um `rand` verschoben und der
|
||||
Kasten entsprechend groesser. Damit liegt eine Flaeche, die bei -rand
|
||||
beginnt, genau auf der Kante des Kastens -- und die Randmessung sieht
|
||||
wieder, was wirklich dasteht. */
|
||||
const svgMitRand = (b, h, rand, inhalt) =>
|
||||
wfSvg(b + 2 * rand, h + 2 * rand,
|
||||
'<g transform="translate(' + rand + ',' + rand + ')">' + inhalt + '</g>');
|
||||
|
||||
const SCHATTEN_DEFS =
|
||||
'<defs><filter id="hgschatten" x="-35%" y="-35%" width="170%" height="170%">' +
|
||||
'<feDropShadow dx="0" dy="7" stdDeviation="10" flood-color="#101828" ' +
|
||||
'flood-opacity="0.14"/></filter></defs>';
|
||||
'<defs>' +
|
||||
'<filter id="hgklein" x="-40%" y="-40%" width="180%" height="180%">' +
|
||||
'<feDropShadow dx="0" dy="1" stdDeviation="1" flood-color="#000000" flood-opacity="0.08"/>' +
|
||||
'</filter>' +
|
||||
'<filter id="hgschatten" x="-45%" y="-45%" width="190%" height="190%">' +
|
||||
'<feDropShadow dx="0" dy="2" stdDeviation="1.6" flood-color="#000000" flood-opacity="0.08"/>' +
|
||||
'<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000000" flood-opacity="0.08"/>' +
|
||||
'</filter>' +
|
||||
'<filter id="hgblur" x="-10%" y="-10%" width="120%" height="120%">' +
|
||||
'<feGaussianBlur stdDeviation="3.6"/>' +
|
||||
'</filter>' +
|
||||
'<filter id="hgtief" x="-45%" y="-45%" width="190%" height="190%">' +
|
||||
'<feDropShadow dx="0" dy="4" stdDeviation="3" flood-color="#000000" flood-opacity="0.08"/>' +
|
||||
'<feDropShadow dx="0" dy="12" stdDeviation="12" flood-color="#000000" flood-opacity="0.10"/>' +
|
||||
'</filter>' +
|
||||
'</defs>';
|
||||
|
||||
// Eine Karte: Flaeche, Haarlinie, Schatten. Das ist die Grundform des Themes.
|
||||
const karteFlaeche = (x, y, w, h, c, fuell) =>
|
||||
@@ -1822,6 +1873,8 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
und der <svg>-Rahmen muss weg, sonst rendert ein einkindriges Zeichen
|
||||
nicht. */
|
||||
const FEATHER = {
|
||||
"hard-hat": "<path d=\"M10 10V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v5\"/> <path d=\"M14 6a6 6 0 0 1 6 6v3\"/> <path d=\"M4 15v-3a6 6 0 0 1 6-6\"/> <rect x=\"2\" y=\"15\" width=\"20\" height=\"4\" rx=\"1\"/>",
|
||||
"building": "<rect width=\"16\" height=\"20\" x=\"4\" y=\"2\" rx=\"2\"/> <path d=\"M9 22v-4h6v4\"/> <path d=\"M8 6h.01\"/> <path d=\"M16 6h.01\"/> <path d=\"M12 6h.01\"/> <path d=\"M12 10h.01\"/> <path d=\"M12 14h.01\"/> <path d=\"M16 10h.01\"/> <path d=\"M16 14h.01\"/> <path d=\"M8 10h.01\"/> <path d=\"M8 14h.01\"/>",
|
||||
"briefcase": "<rect width=\"20\" height=\"14\" x=\"2\" y=\"7\" rx=\"2\" ry=\"2\"/> <path d=\"M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16\"/>",
|
||||
"folder-open": "<path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/>",
|
||||
"mic": "<path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"/> <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/> <line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/>",
|
||||
@@ -2381,6 +2434,20 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
Alle Namen, Zeiten, Vorschautexte und Anhaenge stehen woertlich in CHATS
|
||||
und INITIAL_MESSAGES. Erfunden ist nichts. */
|
||||
const CHAT_TEXT = "#24242B", CHAT_TEXT2 = "#505057", CHAT_TEXT3 = "#858589";
|
||||
/* Die drei Flaechen des Werkzeugs, aufgeloest aus ui/theme.css:
|
||||
surface-neutral-primary grey-50 #F5F4F4 der Grund der Anwendung
|
||||
surface-neutral-tertiary grey-100 #F0F0F0 die Unterhaltungsspalte
|
||||
surface-neutral-secondary grey-0 #FAFAFA Karten und Blasen
|
||||
|
||||
Auf den Slides werden sie nicht als Hintergrund gemalt -- die
|
||||
Zeichnungen bleiben durchsichtig und stehen auf dem Punktgrund der
|
||||
Slide. Gebraucht werden die Werte dort, wo eine Flaeche wirklich eine
|
||||
ist: im Schirm des Geraets auf Slide 5.
|
||||
|
||||
Karten und Blasen stehen auf reinem Weiss statt auf #FAFAFA. Auf dem
|
||||
hellen Blatt der Slide waere der Unterschied sonst nicht mehr zu sehen
|
||||
und die Blase verloere ihre Kante. */
|
||||
const FL_GRUND = "#F5F4F4", FL_SPALTE = "#F0F0F0", FL_KARTE = "#FFFFFF";
|
||||
const CHAT_RAND = "#E5E5E5";
|
||||
|
||||
/* Die Chatliste links. type steuert das Sinnbild davor: ein Projekt bekommt
|
||||
@@ -2420,8 +2487,12 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
function chatZeile(x, y, c) {
|
||||
let g = "";
|
||||
if (c.aktiv) {
|
||||
g += '<rect x="' + n1(x) + '" y="' + n1(y) + '" width="' + LISTE_B + '" height="' +
|
||||
ZEILE_H + '" rx="10" fill="#F0E7FF"/>';
|
||||
/* Der offene Kanal ist hell hinterlegt, mit Rand und feinem Schatten --
|
||||
nicht farbig. Der erste Nachbau hatte ihn violett; im Original steht
|
||||
dort bg-surface-neutral-primary mit border und shadow-sm. */
|
||||
g += '<g filter="url(#hgklein)"><rect x="' + n1(x) + '" y="' + n1(y) + '" width="' +
|
||||
LISTE_B + '" height="' + ZEILE_H + '" rx="10" fill="' + FL_KARTE +
|
||||
'" stroke="' + CHAT_RAND + '" stroke-width="1.2"/></g>';
|
||||
}
|
||||
const mx = x + 16, my = y + ZEILE_H / 2;
|
||||
if (c.art === "direkt") {
|
||||
@@ -2471,9 +2542,16 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
const BLASE_MAX = 470;
|
||||
const blaseBreite = spalte => Math.min(BLASE_MAX, spalte - 60);
|
||||
|
||||
/* Wie breit eine Anhangkarte sein muss, damit ihr Titel ungekuerzt
|
||||
hineinpasst. Ohne diese Rechnung war die Blase nur so breit wie ihr
|
||||
Fliesstext, und "Materialliste pruefen · faellig 31.07.2026" wurde
|
||||
abgeschnitten -- an einer Stelle, an der der Titel die Aussage traegt. */
|
||||
const anhangNoetig = a =>
|
||||
52 + messen(a[1], 15, 600) + (a[3] ? messen(a[3], 13, 800) + 56 : 0) + 12;
|
||||
|
||||
function anhangKarte(x, y, b, ikon, titel, meta, marke) {
|
||||
let g = '<rect x="' + n1(x) + '" y="' + n1(y) + '" width="' + n1(b) + '" height="54" rx="9" ' +
|
||||
'fill="#FFFFFF" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>';
|
||||
'fill="' + FL_KARTE + '" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>';
|
||||
g += '<rect x="' + n1(x + 10) + '" y="' + n1(y + 11) + '" width="32" height="32" rx="8" ' +
|
||||
'fill="#FAFAFA" stroke="' + CHAT_RAND + '" stroke-width="1.1"/>';
|
||||
g += feather(ikon, x + 26, y + 27, 17, CHAT_TEXT2, 1.9);
|
||||
@@ -2496,7 +2574,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
let b = 32 + Math.max(
|
||||
...zeilen.map(z => messen(z, 17, 400)),
|
||||
m.mein ? 0 : messen(m.autor, 14, 800),
|
||||
m.anhang ? 300 : 0, 90);
|
||||
m.anhang ? anhangNoetig(m.anhang) : 0, 90);
|
||||
return { zeilen, hoehe: Math.round(h), breite: Math.round(Math.min(b, max || BLASE_MAX)) };
|
||||
}
|
||||
|
||||
@@ -2520,7 +2598,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
}
|
||||
const kontur = rundRechteck(bx, y, ma.breite, ma.hoehe, 14);
|
||||
g += '<g filter="url(#hgschatten)"><path d="' + kontur + '" fill="' +
|
||||
(m.mein ? "#C8FFF7" : "#FFFFFF") + '"/></g>';
|
||||
(m.mein ? "#C8FFF7" : FL_KARTE) + '"/></g>';
|
||||
if (!m.mein) g += '<path d="' + kontur + '" fill="none" stroke="' + CHAT_RAND +
|
||||
'" stroke-width="1.2"/>';
|
||||
let cy = y + 14;
|
||||
@@ -2564,13 +2642,89 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
];
|
||||
|
||||
|
||||
/* Der Stapel-Look aus post1.html, in SVG nachgebaut. Dort macht ihn CSS
|
||||
ueber zwei Blaetter hinter dem Bild:
|
||||
|
||||
.sheet-mid rotate(-2.4deg) translate(-2.2%, 3%) scale(.975)
|
||||
.sheet-back rotate( 3.2deg) translate( 3%, 5.5%) scale(.95)
|
||||
|
||||
Dieselben Winkel und Verschiebungen, damit die Reihe zusammenpasst. In
|
||||
CSS liegt der Drehpunkt in der Mitte und die Prozente beziehen sich auf
|
||||
die eigene Groesse -- in SVG heisst das
|
||||
translate(mitte) rotate() translate(prozent*groesse) scale() translate(-mitte).
|
||||
|
||||
Der Stapel traegt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein
|
||||
nicht schafft, und er sagt, dass hinter der einen Ansicht weitere
|
||||
liegen. Deshalb sind die hinteren Blaetter leer -- sie sind Andeutung,
|
||||
keine Kopie. */
|
||||
/* Die beiden hinteren Blaetter je: Winkel, Verschiebung in Anteilen der
|
||||
eigenen Groesse, Massstab, Fuellung. Reihenfolge wie in CSS -- erst
|
||||
skalieren, dann verschieben, dann drehen. */
|
||||
const STAPEL_BLAETTER = [
|
||||
{ winkel: 3.2, px: 0.03, py: 0.055, skala: 0.95, fuell: "#F7F7F8" },
|
||||
{ winkel: -2.4, px: -0.022, py: 0.03, skala: 0.975, fuell: "#FCFCFD" },
|
||||
];
|
||||
|
||||
/* Wie weit ragen die gedrehten Blaetter ueber das vordere hinaus? Gerechnet
|
||||
statt geschaetzt: die vier Ecken durch dieselbe Kette schicken und den
|
||||
groessten Ueberstand nehmen. Der erste Anlauf hatte den Ueberstand
|
||||
ignoriert -- die Blaetter standen dann bis zu 24 px ausserhalb der
|
||||
92-px-Schutzzone. */
|
||||
function stapelRand(b, h) {
|
||||
let raus = 0;
|
||||
for (const bl of STAPEL_BLAETTER) {
|
||||
const co = Math.cos(bl.winkel * Math.PI / 180), si = Math.sin(bl.winkel * Math.PI / 180);
|
||||
for (const [ex, ey] of [[-1, -1], [1, -1], [1, 1], [-1, 1]]) {
|
||||
// Ecke relativ zur Mitte: skalieren, verschieben, drehen.
|
||||
const x = ex * b / 2 * bl.skala + bl.px * b;
|
||||
const y = ey * h / 2 * bl.skala + bl.py * h;
|
||||
raus = Math.max(raus, Math.abs(x * co - y * si) - b / 2,
|
||||
Math.abs(x * si + y * co) - h / 2);
|
||||
}
|
||||
}
|
||||
return Math.ceil(raus) + 2;
|
||||
}
|
||||
|
||||
/* Der Stapel-Look aus post1.html, in SVG nachgebaut. Dort macht ihn CSS
|
||||
ueber zwei Blaetter hinter dem Bild, mit genau diesen Winkeln und
|
||||
Verschiebungen -- damit die Reihe zusammenpasst.
|
||||
|
||||
Der Stapel traegt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein
|
||||
nicht schafft, und er sagt, dass hinter der einen Ansicht weitere
|
||||
liegen. Deshalb sind die hinteren Blaetter leer -- sie sind Andeutung,
|
||||
keine Kopie.
|
||||
|
||||
b und h sind die Groesse des vorderen Blattes. Der Kasten drumherum wird
|
||||
um stapelRand() groesser; darum kuemmert sich stapelKasten(). */
|
||||
function stapel(b, h, radius, inhalt) {
|
||||
const m = stapelRand(b, h);
|
||||
const blatt = bl => {
|
||||
const cx = b / 2, cy = h / 2;
|
||||
return '<g transform="translate(' + n1(cx + m) + ',' + n1(cy + m) + ') rotate(' +
|
||||
bl.winkel + ') translate(' + n1(bl.px * b) + ',' + n1(bl.py * h) + ') scale(' +
|
||||
bl.skala + ') translate(' + n1(-cx) + ',' + n1(-cy) + ')">' +
|
||||
'<g filter="url(#hgschatten)"><path d="' + rundRechteck(0, 0, b, h, radius) +
|
||||
'" fill="' + bl.fuell + '" stroke="' + CHAT_RAND + '" stroke-width="1.2"/></g></g>';
|
||||
};
|
||||
return blatt(STAPEL_BLAETTER[0]) + blatt(STAPEL_BLAETTER[1]) +
|
||||
'<g transform="translate(' + m + ',' + m + ')">' +
|
||||
'<g filter="url(#hgtief)"><path d="' + rundRechteck(0, 0, b, h, radius) +
|
||||
'" fill="#FFFFFF" stroke="' + CHAT_RAND + '" stroke-width="1.2"/></g>' + inhalt + '</g>';
|
||||
}
|
||||
|
||||
// Der Kasten, in den ein Stapel dieser Groesse hineinpasst.
|
||||
const stapelKasten = (b, h, radius, inhalt) => {
|
||||
const m = stapelRand(b, h);
|
||||
return wfSvg(b + 2 * m, h + 2 * m, stapel(b, h, radius, inhalt));
|
||||
};
|
||||
|
||||
/* ─── Slide 1 · Der Chat ─────────────────────────────────────────────────
|
||||
Kanalliste links, Verlauf rechts. Im Profilraster ist die Slide eine
|
||||
Kachel von rund 150 px -- dort ist kein Wort lesbar, und was ankommt,
|
||||
sind zwei Spalten mit Blasen. Daran erkennt jemand einen Chat, bevor er
|
||||
etwas gelesen hat. Dieselbe Erwaegung wie bei Post 3, 4 und 5. */
|
||||
function visChat() {
|
||||
const SP = 26, RECHTS = 440, W = LISTE_B + SP + RECHTS;
|
||||
const SP = 26, RECHTS = 460, W = LISTE_B + SP + RECHTS, PAD = 26;
|
||||
let g = "";
|
||||
|
||||
/* Die Filterzeile ueber der Liste. Die vier Woerter sind der Typ
|
||||
@@ -2579,18 +2733,35 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
["Alle", "Projekte", "Direkt", "Ungelesen"].forEach((f, i) => {
|
||||
const b = Math.round(messen(f, 15, 700)) + 26;
|
||||
g += '<rect x="' + n1(fx) + '" y="0" width="' + b + '" height="32" rx="8" fill="' +
|
||||
(i === 0 ? "#24242B" : "#FFFFFF") + '" stroke="' + (i === 0 ? "#24242B" : CHAT_RAND) +
|
||||
(i === 0 ? "#24242B" : FL_KARTE) + '" stroke="' + (i === 0 ? "#24242B" : CHAT_RAND) +
|
||||
'" stroke-width="1.2"/>';
|
||||
g += uiText(fx + b / 2, 21, f, 15, 700, i === 0 ? "#FFFFFF" : CHAT_TEXT2, { anker: "middle" });
|
||||
fx += b + 8;
|
||||
});
|
||||
|
||||
/* Die Unterhaltungsspalte liegt auf eigener Flaeche, eine Stufe dunkler
|
||||
als die Blasen darauf. Das ist die Tiefe, die vorher fehlte: ohne sie
|
||||
schwebten weisse Blasen auf weissem Grund. */
|
||||
const listeO = 50;
|
||||
CHATS.forEach((c, i) => { g += chatZeile(0, listeO + i * ZEILE_H, c); });
|
||||
const listeH = listeO + CHATS.length * ZEILE_H;
|
||||
|
||||
// Die Kopfzeile des offenen Kanals, wie in ChatHeader.
|
||||
const kx = LISTE_B + SP;
|
||||
/* Keine Flaeche hinter der Unterhaltung. Ein Zwischenstand hatte hier
|
||||
drei graue Stufen liegen, um Tiefe zu erzeugen -- das nimmt den
|
||||
Zeichnungen aber ihre Durchsichtigkeit und setzt einen Kasten auf den
|
||||
Punktgrund, der da nicht hingehoert. Die Tiefe kommt jetzt allein aus
|
||||
den gestaffelten Schatten und daraus, dass die Blasen einen Rand
|
||||
tragen und der offene Kanal als Karte liegt.
|
||||
|
||||
Die duenne Linie zwischen Liste und Unterhaltung bleibt: sie steht so
|
||||
im Werkzeug (border-r an der Chatliste) und trennt zwei Spalten, ohne
|
||||
eine Flaeche zu behaupten. */
|
||||
g += '<path d="M' + n1(kx - 14) + ',-6V' + n1(listeH) + '" stroke="' + CHAT_RAND +
|
||||
'" stroke-width="1.2"/>';
|
||||
g = '<g transform="translate(' + PAD + ',' + PAD + ')">' + g + '</g>';
|
||||
|
||||
// Die Kopfzeile des offenen Kanals, wie in ChatHeader.
|
||||
g += '<rect x="' + n1(kx) + '" y="0" width="40" height="40" rx="11" fill="#F0E7FF" ' +
|
||||
'stroke="#e4d4ff" stroke-width="1.2"/>';
|
||||
g += feather("briefcase", kx + 20, 20, 20, "#691CD4", 1.9);
|
||||
@@ -2602,24 +2773,46 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
/* Der Verlauf laeuft bis kurz vor das Eingabefeld. Was nicht mehr
|
||||
hineinpasst, faellt weg -- ein Chat ist oben abgeschnitten, nicht
|
||||
unten, aber fuer ein Standbild ist die Leserichtung wichtiger. */
|
||||
const feldH = 64, platz = listeH - feldH - 20;
|
||||
let y = 76;
|
||||
const feldH = 64, tippH = 40, platz = listeH - feldH - tippH - 16;
|
||||
|
||||
/* Die Datumsmarke steht so ueber dem ersten Beitrag des Tages, als
|
||||
Badge mit dem Ton brand-primary-subtle. Sie kostet wenig und sagt,
|
||||
dass man einen Verlauf sieht und keine Liste. */
|
||||
const dt = "Heute · 30.07.2026";
|
||||
const dtB = Math.round(messen(dt, 14, 700)) + 28;
|
||||
g += '<rect x="' + n1(kx + RECHTS / 2 - dtB / 2) + '" y="72" width="' + dtB +
|
||||
'" height="30" rx="15" fill="#FCF6C5" stroke="#faeb8e" stroke-width="1.2"/>' +
|
||||
uiText(kx + RECHTS / 2, 92, dt, 14, 700, "#9B5E0D", { anker: "middle" });
|
||||
|
||||
let y = 118;
|
||||
for (const m of VERLAUF) {
|
||||
if (y + blaseHoehe(m, RECHTS) > platz) break;
|
||||
g += blase(kx, y, RECHTS, m);
|
||||
y += blaseHoehe(m, RECHTS) + 14;
|
||||
}
|
||||
|
||||
/* "Maike Berger schreibt gerade …" steht im Werkzeug als eigene Zeile
|
||||
ueber dem Eingabefeld. Sie sagt in vier Woertern, dass da jemand
|
||||
mitliest -- das traegt eine Standaufnahme besser als jede Bewegung. */
|
||||
const tt = "Maike Berger schreibt gerade …";
|
||||
const ttB = Math.round(messen(tt, 14, 500)) + 32;
|
||||
const ty = listeH - feldH - tippH + 2;
|
||||
g += '<g filter="url(#hgklein)"><rect x="' + n1(kx) + '" y="' + n1(ty) + '" width="' +
|
||||
ttB + '" height="32" rx="16" fill="' + FL_KARTE + '" stroke="' + CHAT_RAND +
|
||||
'" stroke-width="1.1"/></g>' +
|
||||
uiText(kx + 16, ty + 21, tt, 14, 500, CHAT_TEXT3);
|
||||
|
||||
// Das Eingabefeld unten, wie in Composer.
|
||||
const fy = listeH - feldH;
|
||||
g += '<path d="M' + n1(kx) + ',' + n1(fy) + 'H' + n1(kx + RECHTS) + '" stroke="' +
|
||||
CHAT_RAND + '" stroke-width="1.2"/>';
|
||||
g += feather("plus", kx + 20, fy + 34, 21, CHAT_TEXT2, 2);
|
||||
g += '<rect x="' + n1(kx + 42) + '" y="' + n1(fy + 14) + '" width="' + (RECHTS - 84) +
|
||||
'" height="40" rx="20" fill="#FFFFFF" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>';
|
||||
g += '<g filter="url(#hgklein)"><rect x="' + n1(kx + 42) + '" y="' + n1(fy + 14) +
|
||||
'" width="' + (RECHTS - 84) + '" height="40" rx="20" fill="' + FL_KARTE +
|
||||
'" stroke="' + CHAT_RAND + '" stroke-width="1.2"/></g>';
|
||||
g += uiText(kx + 62, fy + 40, "Nachricht schreiben …", 16, 400, "#C9C9CE");
|
||||
g += feather("mic", kx + RECHTS - 20, fy + 34, 21, CHAT_TEXT2, 2);
|
||||
return wfSvg(W, listeH, g);
|
||||
return stapelKasten(W + 2 * PAD, listeH + 2 * PAD, 22, g);
|
||||
}
|
||||
|
||||
/* ─── Slide 2 · Die Kanäle ───────────────────────────────────────────────
|
||||
@@ -2632,9 +2825,10 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
Direktnachricht, Gruppe -- und lassen die Zeilen gross genug, dass die
|
||||
Projektnummer lesbar bleibt. Eine Liste scrollt ohnehin. */
|
||||
const zeigen = CHATS.slice(0, 5);
|
||||
const hoehe = zeigen.length * ZEILE_H, PAD = 22;
|
||||
let g = "";
|
||||
zeigen.forEach((c, i) => { g += chatZeile(0, i * ZEILE_H, c); });
|
||||
return wfSvg(LISTE_B, zeigen.length * ZEILE_H, g);
|
||||
zeigen.forEach((c, i) => { g += chatZeile(PAD, PAD + i * ZEILE_H, c); });
|
||||
return stapelKasten(LISTE_B + 2 * PAD, hoehe + 2 * PAD, 20, g);
|
||||
}
|
||||
|
||||
/* ─── Slide 3 · Aus der Nachricht wird die Aufgabe ───────────────────────
|
||||
@@ -2643,13 +2837,16 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
sie im Projekt liegt. Das ist der Satz aus dem Redaktionsplan --
|
||||
"Foto rein, Aufgabe raus" -- und er steht so im Werkzeug. */
|
||||
function visAufgabe() {
|
||||
const W = 520;
|
||||
let g = "", y = 0;
|
||||
/* Schmaler gesetzt als die Spalte auf Slide 1: dadurch skaliert die
|
||||
Zeichnung groesser und fuellt die Flaeche, statt mit einem Handbreit
|
||||
Leere darueber zu stehen. */
|
||||
const W = 470;
|
||||
let inhalt = "", y = 0;
|
||||
for (const m of VERLAUF.slice(2, 5)) {
|
||||
g += blase(0, y, W, m);
|
||||
inhalt += blase(0, y, W, m);
|
||||
y += blaseHoehe(m, W) + 18;
|
||||
}
|
||||
return wfSvg(W, y - 18, g);
|
||||
return wfSvg(W, y - 18, inhalt);
|
||||
}
|
||||
|
||||
/* ─── Slide 4 · Was noch in den Chat passt ───────────────────────────────
|
||||
@@ -2670,7 +2867,9 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
const W = 470, ZH = 52, PAD = 14;
|
||||
const hoehe = 2 * PAD + MENUE.reduce((a, e) => a + (e ? ZH : 13), 0);
|
||||
const kontur = rundRechteck(0, 0, W, hoehe, 14);
|
||||
let g = '<g filter="url(#hgschatten)"><path d="' + kontur + '" fill="#FFFFFF"/>' +
|
||||
/* Ein aufgeklapptes Menue liegt ueber allem anderen. Im Werkzeug traegt
|
||||
es --shadow-overlay, also dieselbe Staffelung wie --shadow-lg. */
|
||||
let g = '<g filter="url(#hgtief)"><path d="' + kontur + '" fill="' + FL_KARTE + '"/>' +
|
||||
'<path d="' + kontur + '" fill="none" stroke="' + CHAT_RAND + '" stroke-width="1.3"/></g>';
|
||||
let y = PAD;
|
||||
for (const e of MENUE) {
|
||||
@@ -2687,18 +2886,158 @@ if (location.search.indexOf("nur=streifen") >= 0)
|
||||
es schwebt die Liste im Nichts und man sieht nicht, woher sie kommt. */
|
||||
const fy = hoehe + 18;
|
||||
g += feather("plus", 20, fy + 26, 22, CHAT_TEXT2, 2);
|
||||
g += '<rect x="44" y="' + n1(fy + 4) + '" width="' + (W - 88) + '" height="44" rx="22" ' +
|
||||
'fill="#FFFFFF" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>';
|
||||
g += '<g filter="url(#hgklein)"><rect x="44" y="' + n1(fy + 4) + '" width="' + (W - 88) +
|
||||
'" height="44" rx="22" fill="' + FL_KARTE + '" stroke="' + CHAT_RAND +
|
||||
'" stroke-width="1.2"/></g>';
|
||||
g += uiText(64, fy + 33, "Nachricht schreiben …", 17, 400, "#C9C9CE");
|
||||
g += feather("mic", W - 20, fy + 26, 22, CHAT_TEXT2, 2);
|
||||
return wfSvg(W, fy + 52, g);
|
||||
}
|
||||
|
||||
|
||||
/* ─── Slide 5 · Der Chat für unterwegs ──────────────────────────────────
|
||||
Diese Slide erfindet nichts: die Website hat den Zustand bereits, in
|
||||
components/sections/ChatDemoSection.tsx unter der Fahne `comingSoon`.
|
||||
Dort steht ein Handy-Mockup, das verwischt wird (blur(3px)), und
|
||||
darueber liegt ein Absperrband -- gedreht, mit Schutzhelm und der Zeile
|
||||
"Bald verfuegbar", wieder und wieder.
|
||||
|
||||
Ich hatte das zuerst selbst gebaut, mit einem gelben Aufkleber "In
|
||||
Arbeit". Das war eine eigene Erfindung neben einer vorhandenen
|
||||
Bildsprache. Uebernommen ist jetzt die der Website, samt Winkel (-13°),
|
||||
Farben (#F1B729 auf #171106) und Wortlaut.
|
||||
|
||||
Auch der Inhalt des Schirms kommt von dort und nicht aus dem Pitch-Paket:
|
||||
Heizungsprojekt Schulz, 4 Mitglieder · 2 online, die Nachrichten aus
|
||||
seedMessages. Auf der Website steht dieses Handy fuer den mobilen Chat --
|
||||
dann soll es hier dasselbe zeigen. */
|
||||
const WEB = {
|
||||
grund: "#F5F2EF", // CHAT_BACKGROUND, hsl(30 17% 95.5%)
|
||||
eigen: "#0D6E6E", // OWN_BUBBLE_BACKGROUND
|
||||
eigenR: "#0A5A5A", // OWN_BUBBLE_BORDER
|
||||
fremd: "#FFFFFF", // OTHER_BUBBLE_BACKGROUND
|
||||
fremdR: "#E9E7E5", // OTHER_BUBBLE_BORDER
|
||||
zeit: "#7A726C", // OTHER_BUBBLE_TIME
|
||||
sender: "#F59F0A", // OTHER_BUBBLE_SENDER
|
||||
rahmen: "#D1D5DB", // border des Mockups
|
||||
band: "#F1B729", // accent-yellow
|
||||
bandDunkel: "#171106",
|
||||
};
|
||||
|
||||
// seedMessages aus ChatDemoSection.tsx, woertlich und in dieser Reihenfolge.
|
||||
const WEB_NACHRICHTEN = [
|
||||
["Murat Kaya", "Foto vom Heizungskeller hochgeladen.", "09:12", false],
|
||||
["Lea Brandt", "Aufmaß ist fertig, schicke gleich die Unterlagen.", "09:15", false],
|
||||
["Du", "Super, dann können wir morgen mit der Installation anfangen.", "09:17", true],
|
||||
["Murat Kaya", "Material ist bestellt, Lieferung Donnerstag.", "09:20", false],
|
||||
];
|
||||
|
||||
function visHandy() {
|
||||
/* Das Geraet. Die Website zeichnet nur einen hellen Umriss (border 4px
|
||||
#D1D5DB) -- auf einer Slide, die selbst hell ist, faellt der in sich
|
||||
zusammen und sieht nach Platzhalter aus. Hier steht deshalb ein
|
||||
richtiges Geraet: dunkler Korpus, heller Grat darauf, Schirm
|
||||
eingelassen, Tasten an der Seite. Das ist eine Abweichung, aber eine
|
||||
am Rahmen und nicht am Inhalt -- was auf dem Schirm steht, bleibt das
|
||||
der Website.
|
||||
|
||||
Der Schirm wird weniger stark verwischt als dort. Auf der Website
|
||||
liegt das Handy gross neben Fliesstext und darf unlesbar sein; hier
|
||||
ist es klein, und bei blur(6) blieb nur Brei. Erkennbar, aber nicht
|
||||
lesbar ist die Absicht. */
|
||||
const B = 600, H = 1240, BEZEL = 20, R = 92;
|
||||
const sx = BEZEL, sy = BEZEL, sb = B - 2 * BEZEL, sh = H - 2 * BEZEL, sr = R - BEZEL + 4;
|
||||
let schirm = "";
|
||||
|
||||
// Statuszeile und Kopf.
|
||||
schirm += uiText(sx + 40, sy + 56, "9:41", 26, 700, CHAT_TEXT);
|
||||
const ko = sy + 84;
|
||||
schirm += '<rect x="' + n1(sx) + '" y="' + n1(sy) + '" width="' + sb +
|
||||
'" height="' + (ko - sy + 112) + '" fill="#FFFFFF"/>';
|
||||
schirm += uiText(sx + 40, sy + 56, "9:41", 26, 700, CHAT_TEXT);
|
||||
schirm += '<circle cx="' + n1(sx + 64) + '" cy="' + n1(ko + 56) + '" r="34" fill="#F3E4C2"/>';
|
||||
schirm += feather("building", sx + 64, ko + 56, 30, "#8A6116", 2.2);
|
||||
schirm += uiText(sx + 110, ko + 50, "Heizungsprojekt Schulz", 27, 700, CHAT_TEXT);
|
||||
schirm += uiText(sx + 110, ko + 80, "4 Mitglieder · 2 online", 21, 500, CHAT_TEXT2);
|
||||
schirm += '<path d="M' + n1(sx) + ',' + n1(ko + 112) + 'H' + n1(sx + sb) +
|
||||
'" stroke="#E5E7EB" stroke-width="2"/>';
|
||||
|
||||
let y = ko + 142;
|
||||
for (const [wer, text, zeit, eigen] of WEB_NACHRICHTEN) {
|
||||
const maxB = sb - 88;
|
||||
const zeilen = umbrechen(text, maxB - 40, 24, 400);
|
||||
const h = 20 + (eigen ? 0 : 26) + zeilen.length * 32 + 26;
|
||||
const breite = 40 + Math.max(...zeilen.map(z => messen(z, 24, 400)),
|
||||
eigen ? 0 : messen(wer, 19, 700));
|
||||
const x0 = eigen ? sx + sb - 28 - breite : sx + 28;
|
||||
const k = rundRechteck(x0, y, breite, h, 20);
|
||||
schirm += '<path d="' + k + '" fill="' + (eigen ? WEB.eigen : WEB.fremd) +
|
||||
'" stroke="' + (eigen ? WEB.eigenR : WEB.fremdR) + '" stroke-width="2"/>';
|
||||
let cy = y + 20;
|
||||
if (!eigen) { schirm += uiText(x0 + 20, cy + 19, wer, 19, 700, WEB.sender); cy += 26; }
|
||||
zeilen.forEach((z, i) => {
|
||||
schirm += uiText(x0 + 20, cy + 25 + i * 32, z, 24, 400, eigen ? "#FFFFFF" : CHAT_TEXT);
|
||||
});
|
||||
schirm += uiText(x0 + 20, cy + 25 + zeilen.length * 32, zeit, 18, 400,
|
||||
eigen ? "rgba(255,255,255,0.82)" : WEB.zeit);
|
||||
y += h + 18;
|
||||
}
|
||||
|
||||
const fy = sy + sh - 92;
|
||||
schirm += '<rect x="' + n1(sx) + '" y="' + n1(fy - 18) + '" width="' + sb +
|
||||
'" height="110" fill="#FFFFFF"/>';
|
||||
schirm += '<rect x="' + n1(sx + 28) + '" y="' + n1(fy) + '" width="' + (sb - 56) +
|
||||
'" height="66" rx="33" fill="' + WEB.grund + '" stroke="#E5E7EB" stroke-width="2"/>';
|
||||
schirm += uiText(sx + 54, fy + 43, "Nachricht schreiben...", 23, 400, "#A8A29E");
|
||||
// Der Balken unten, den jedes heutige Geraet hat.
|
||||
schirm += '<rect x="' + n1(B / 2 - 70) + '" y="' + n1(sy + sh - 16) +
|
||||
'" width="140" height="6" rx="3" fill="#C9C9CE"/>';
|
||||
|
||||
/* Der Korpus. Tasten zuerst, damit sie hinter dem Rand verschwinden. */
|
||||
let g = '';
|
||||
[[-6, 300, 90], [-6, 420, 130], [B, 360, 150]].forEach(([tx, ty2, th]) => {
|
||||
g += '<rect x="' + n1(tx) + '" y="' + n1(ty2) + '" width="8" height="' + th +
|
||||
'" rx="4" fill="#3A3B42"/>';
|
||||
});
|
||||
g += '<g filter="url(#hgtief)"><path d="' + rundRechteck(0, 0, B, H, R) +
|
||||
'" fill="#1C1D22"/></g>';
|
||||
g += '<path d="' + rundRechteck(5, 5, B - 10, H - 10, R - 5) +
|
||||
'" fill="none" stroke="#4A4B54" stroke-width="2"/>';
|
||||
g += '<clipPath id="hgschirm"><path d="' + rundRechteck(sx, sy, sb, sh, sr) + '"/></clipPath>';
|
||||
g += '<path d="' + rundRechteck(sx, sy, sb, sh, sr) + '" fill="' + WEB.grund + '"/>';
|
||||
g += '<g clip-path="url(#hgschirm)" filter="url(#hgblur)">' + schirm + '</g>';
|
||||
// Die Insel oben.
|
||||
g += '<rect x="' + n1(B / 2 - 62) + '" y="' + n1(sy + 14) +
|
||||
'" width="124" height="34" rx="17" fill="#0B0B0E"/>';
|
||||
|
||||
/* Das Absperrband, Winkel und Wortlaut der Website. */
|
||||
const bh = 64, ty = H / 2 - bh / 2;
|
||||
const bL = B / Math.cos(13 * Math.PI / 180) + 24, bA = (B - bL) / 2;
|
||||
let band = '<rect x="' + n1(bA) + '" y="' + n1(ty) + '" width="' + n1(bL) +
|
||||
'" height="' + bh + '" fill="' + WEB.band + '"/>' +
|
||||
'<path d="M' + n1(bA) + ',' + n1(ty) + 'h' + n1(bL) + '" stroke="' + WEB.bandDunkel +
|
||||
'" stroke-width="4"/>' +
|
||||
'<path d="M' + n1(bA) + ',' + n1(ty + bh) + 'h' + n1(bL) + '" stroke="' + WEB.bandDunkel +
|
||||
'" stroke-width="4"/>';
|
||||
const wort = "BALD VERFÜGBAR";
|
||||
const wb = messen(wort, 21, 800) + 21 * 0.16 * wort.length;
|
||||
let bx = bA + 14;
|
||||
while (bx < bA + bL - 40) {
|
||||
band += feather("hard-hat", bx + 13, ty + bh / 2, 24, WEB.bandDunkel, 2.6);
|
||||
band += uiText(bx + 32, ty + bh / 2 + 8, wort, 21, 800, WEB.bandDunkel, { sperr: "0.16em" });
|
||||
bx += wb + 78;
|
||||
}
|
||||
g += '<g clip-path="url(#hgschirm)"><g transform="rotate(-13,' + n1(B / 2) + ',' +
|
||||
n1(H / 2) + ')" filter="url(#hgtief)">' + band + '</g></g>';
|
||||
return wfSvg(B, H, g);
|
||||
}
|
||||
|
||||
const VIS = {
|
||||
chat: () => visChat(),
|
||||
kanaele: () => visKanaele(),
|
||||
aufgabe: () => visAufgabe(),
|
||||
menue: () => visMenue(),
|
||||
handy: () => visHandy(),
|
||||
};
|
||||
|
||||
/* ─── Das Deck ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"bg": "raster",
|
||||
"int": "dezent"
|
||||
},
|
||||
{
|
||||
"bg": "raster",
|
||||
"int": "dezent"
|
||||
},
|
||||
{
|
||||
"bg": "raster",
|
||||
"int": "dezent"
|
||||
@@ -35,6 +39,10 @@
|
||||
{
|
||||
"ref": "menue",
|
||||
"werte": {}
|
||||
},
|
||||
{
|
||||
"ref": "handy",
|
||||
"werte": {}
|
||||
}
|
||||
],
|
||||
"edits": [
|
||||
@@ -53,6 +61,11 @@
|
||||
"dy": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"headline": {
|
||||
"dy": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"headline": {
|
||||
"dy": 3
|
||||
@@ -64,6 +77,7 @@
|
||||
"standard",
|
||||
"standard",
|
||||
"standard",
|
||||
"standard",
|
||||
"standard"
|
||||
],
|
||||
"rasterAn": "punkte",
|
||||
|
||||
Reference in new issue
Block a user