diff --git a/.gitignore b/.gitignore index 438657a..261529e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Rohfotos der Beispiel-Bäckerei (werden von scripts/relaunch-baeckerei-fotos.py von Wikimedia Commons geladen) +docs/relaunch/assets/baeckerei/roh/ diff --git a/docs/relaunch/assets/baeckerei/QUELLEN.md b/docs/relaunch/assets/baeckerei/QUELLEN.md new file mode 100644 index 0000000..b349be5 --- /dev/null +++ b/docs/relaunch/assets/baeckerei/QUELLEN.md @@ -0,0 +1,17 @@ +# Fotos der Beispiel-Website „Bäckerei Sonnenschein“ + +Die Bäckerei ist erfunden (Demo-Kundin des Kundenportals). Die Fotos zeigen nur Backwaren, keine Menschen. +Alle stammen von Wikimedia Commons und stehen unter **CC0 1.0** (gemeinfrei, keine Namensnennung nötig). +Verkleinert und zugeschnitten mit `scripts/relaunch-baeckerei-fotos.py` nach `public/relaunch/demos/baeckerei/`. + +| Datei in public | Commons-Datei | +| --- | --- | +| roggenbrot-600.webp | [Rye bread Roggenbengel 2025-02-14 13.jpg](https://commons.wikimedia.org/wiki/File:Rye_bread_Roggenbengel_2025-02-14_13.jpg) | +| roggenbrot-anschnitt-400.webp | [Cut rye bread Kartoffelkruste 2025-02-20 02.jpg](https://commons.wikimedia.org/wiki/File:Cut_rye_bread_Kartoffelkruste_2025-02-20_02.jpg) | +| broetchen-400.webp | [Bread-rolls.jpg](https://commons.wikimedia.org/wiki/File:Bread-rolls.jpg) | +| croissants-400.webp | [Freshippo croissant bread.jpg](https://commons.wikimedia.org/wiki/File:Freshippo_croissant_bread.jpg) | +| zimtschnecken-400.webp | [IKEA cinnamon bun.jpg](https://commons.wikimedia.org/wiki/File:IKEA_cinnamon_bun.jpg) (Ausschnitt ohne Logo oder Schrift) | +| baguettes-700.webp | [Baguette-246424.jpg](https://commons.wikimedia.org/wiki/File:Baguette-246424.jpg) | +| zwetschgenkuchen-520.webp | [Plum cake, Ehrenbach.jpg](https://commons.wikimedia.org/wiki/File:Plum_cake,_Ehrenbach.jpg) | + +Lizenz von jeder Datei am 2026-10-04 über die Commons-API geprüft (`LicenseShortName` = CC0). diff --git a/docs/relaunch/assets/portal/desktop-3c-chat-leer.webp b/docs/relaunch/assets/portal/desktop-3c-chat-leer.webp new file mode 100644 index 0000000..d8d0047 Binary files /dev/null and b/docs/relaunch/assets/portal/desktop-3c-chat-leer.webp differ diff --git a/docs/relaunch/assets/portal/phone-1-projekte.webp b/docs/relaunch/assets/portal/phone-1-projekte.webp index e7be163..af187a2 100644 Binary files a/docs/relaunch/assets/portal/phone-1-projekte.webp and b/docs/relaunch/assets/portal/phone-1-projekte.webp differ diff --git a/docs/relaunch/assets/portal/phone-2-projekte-lang.webp b/docs/relaunch/assets/portal/phone-2-projekte-lang.webp index 13835ca..31dbe21 100644 Binary files a/docs/relaunch/assets/portal/phone-2-projekte-lang.webp and b/docs/relaunch/assets/portal/phone-2-projekte-lang.webp differ diff --git a/docs/relaunch/assets/portal/phone-4c-chat-leer.webp b/docs/relaunch/assets/portal/phone-4c-chat-leer.webp new file mode 100644 index 0000000..1ee486d Binary files /dev/null and b/docs/relaunch/assets/portal/phone-4c-chat-leer.webp differ diff --git a/docs/superpowers/specs/2026-10-04-vorschau-chat.md b/docs/superpowers/specs/2026-10-04-vorschau-chat.md new file mode 100644 index 0000000..04d5e1d --- /dev/null +++ b/docs/superpowers/specs/2026-10-04-vorschau-chat.md @@ -0,0 +1,63 @@ +# Kundenportal-Bühne: Live-Vorschau, Chat-Demo und weiches Zuklappen + +Stand 2026-10-04. Wunsch des Inhabers (diktiert): Das Durchscrollen der Bühne von „Live-Vorschau“ bis „Als App“ soll schöner werden. + +1. Die Testseite in der Live-Vorschau wirkt primitiv. Sie soll wie eine richtige Website aussehen, auch übergreifend im Chat. +2. Beim Herausscrollen aus „Funktionen“ soll das Zuklappen der Unterpunkte schöner sein. +3. Der Chat bekommt eine erklärende Animation: Nachrichten ploppen auf, es wird hineingezoomt, damit man sie lesen kann. + Die Texte sollen spielerisch sein, nicht kalt. + +## 1. Live-Vorschau + +- Im MacBook steht jetzt die Beispiel-Website selbst (`demos/baeckerei/BaeckereiSeite.tsx`), nicht mehr die Portal-Aufnahme. + Die Adresse in der Safari-Leiste ist `baeckerei-sonnenschein.project.webklar.com`, also dieselbe wie im Portal. +- Die Seite ist eine ganze Website: Leiste, Kopf, Startbereich („Frisch gebacken. Jeden Morgen.“), Band, Sortiment mit vier + Karten, Backstube, Torten, Öffnungszeiten mit Stadtplan und Fuß. Die Schrift ist Instrument Serif (selbst gehostet, OFL) + zusammen mit Instrument Sans. +- Die Fotos stammen von Wikimedia Commons und stehen unter CC0. Sie zeigen nur Backwaren, keine Menschen. + Quellen stehen in `docs/relaunch/assets/baeckerei/QUELLEN.md`, das Skript ist `scripts/relaunch-baeckerei-fotos.py`. +- Scroll-Erlebnis: Die Station ist länger (`--kp-l-vorschau: 110svh`). Beim Scrollen durch die Station läuft die Website im + MacBook einmal von oben nach unten durch. Die Bühne schreibt dafür `--kp-v` (0 bis 1); bewegt wird nur `transform`. + Bei reduzierter Bewegung steht die Website oben, und die Station ist kurz. +- Das iPhone zeigt die Projektübersicht des Portals. Die Aufnahme ist neu und zeigt dieselbe Website im Vorschaukasten. + Das gilt auch für die lange Aufnahme in „Als App“. Aufgenommen wurde mit der Fixture des Portals; `demo-site.html` wird mit + `gen-baeckerei.mjs` aus der Komponente erzeugt. +- Hinweiskarte: „Geschützt: Ihre Vorschau öffnet sich nur nach der Anmeldung im Kundenportal – Fremde sehen nichts.“ + Das stimmt: `/api/auth/preview-access` lässt nur Kundensitzungen des Projekts und das Team durch. + +## 2. Chat-Demo + +- Im MacBook ist der Portal-Chat zu sehen. Darunter liegt die Aufnahme des leeren Chats (`desktop-3c-chat-leer`, auch in + 2400 px, damit sie beim Zoomen scharf bleibt). Darüber liegt eine Live-Ebene (`demos/chat/`) mit den Blasen, dem + Eingabefeld, dem Anhang, der Sprachaufnahme, dem Senden-Knopf und einem Zeiger. Formen und Farben sind wie im Portal. +- Ablauf (etwa 28 s, Schleife): Zuerst die Übersicht, dann zoomt die Kamera auf das Gespräch (1,8-fach, nur `transform`). + Marlene hängt ein Foto an und tippt Zeichen für Zeichen. Danach wird gesendet, Kenso antwortet, und Marlene schickt eine + Sprachnachricht. Es folgen noch eine Antwort, eine Nachricht von Marlene und die letzte Antwort. Dann zoomt die Kamera + heraus, und die Nachrichten blenden aus. + Neue Blasen ploppen aus ihrer Ecke auf und federn leicht nach. Die älteren Blasen rutschen weich nach oben (FLIP). +- Texte (Sie-Form vom Team, warm und spielerisch): + - Marlene: „Guten Morgen! ☀️ Unser neues Roggenbrot ist gerade aus dem Ofen. Darf das auf die Startseite?“ (mit Foto) + - Kenso: „Und wie das darf! 😍 Das Foto ist so knusprig, ich rieche es bis hierher.“ + - Marlene: Sprachnachricht 0:12 + - Kenso: „Alles notiert: Brot ganz nach oben, Preis dazu, Öffnungszeiten etwas größer. Schauen Sie mal in die Vorschau! 🥖“ + - Marlene: „Sieht super aus! Morgen gibt’s Croissants fürs ganze Team. 🥐“ + - Kenso: „Dann stellen wir schon mal den Kaffee an. ☕“ +- Das Foto im Chat ist dasselbe Roggenbrot wie auf der Website, denn die Geschichte geht über beide Stationen. +- Die Uhrzeiten (07:52 bis 10:31) versprechen keine Sofort-Antwort. Ein Tippen-Hinweis des Teams wird nicht gezeigt, weil + das Portal keinen hat. +- In der Station Chat gleitet das iPhone nach rechts unten weg. Sonst würde es die eigenen Nachrichten und den + Senden-Knopf verdecken. +- Hinweiskarte: „Antwortet das Team, kommt zusätzlich eine E-Mail – so verpassen Sie keine Nachricht.“ + Das stimmt: Bei jeder Team-Nachricht geht die Mail K17 hinaus, gedrosselt. +- In der Wisch-Reihe auf dem Handy läuft dasselbe Gespräch über der Handy-Aufnahme des leeren Chats, dort ohne Zoom. +- Bei reduzierter Bewegung steht sofort der Endzustand da: alle Nachrichten, hineingezoomt. + +## 3. Weiches Auf- und Zuklappen von „Funktionen“ + +- Zustand `zu`, `offen` oder `schliesst`. Beim Verlassen der drei Funktionen falten sich die Unterpunkte von unten nach + oben ein, und die Linie links zieht sich nach oben zurück. Das dauert etwa 260 ms. Danach verschwindet die Unterliste. +- „Als App“ springt dabei nicht, sondern gleitet per FLIP von der alten an die neue Stelle (Web Animations API, nur + `transform`). Beim Aufklappen gleitet es genauso nach unten, und die Unterpunkte gleiten nacheinander herein. +- Ein kleiner Pfeil hinter „Funktionen“ zeigt den Zustand: nach rechts heißt zu, nach unten heißt offen. +- Für Screenreader ist die Gruppe sofort zu (`aria-expanded="false"`, die Unterliste ist während des Einfaltens + `aria-hidden`). Bei reduzierter Bewegung klappt die Gruppe sofort zu. diff --git a/public/fonts/instrument-serif/LICENSE b/public/fonts/instrument-serif/LICENSE new file mode 100644 index 0000000..af75341 --- /dev/null +++ b/public/fonts/instrument-serif/LICENSE @@ -0,0 +1,93 @@ +Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/instrument-serif/instrument-serif-latin-400-normal.woff2 b/public/fonts/instrument-serif/instrument-serif-latin-400-normal.woff2 new file mode 100644 index 0000000..0ad6971 Binary files /dev/null and b/public/fonts/instrument-serif/instrument-serif-latin-400-normal.woff2 differ diff --git a/public/fonts/instrument-serif/instrument-serif-latin-ext-400-normal.woff2 b/public/fonts/instrument-serif/instrument-serif-latin-ext-400-normal.woff2 new file mode 100644 index 0000000..0caad58 Binary files /dev/null and b/public/fonts/instrument-serif/instrument-serif-latin-ext-400-normal.woff2 differ diff --git a/public/relaunch/demos/baeckerei/baguettes-700.webp b/public/relaunch/demos/baeckerei/baguettes-700.webp new file mode 100644 index 0000000..6a784f5 Binary files /dev/null and b/public/relaunch/demos/baeckerei/baguettes-700.webp differ diff --git a/public/relaunch/demos/baeckerei/broetchen-400.webp b/public/relaunch/demos/baeckerei/broetchen-400.webp new file mode 100644 index 0000000..58cb46f Binary files /dev/null and b/public/relaunch/demos/baeckerei/broetchen-400.webp differ diff --git a/public/relaunch/demos/baeckerei/croissants-400.webp b/public/relaunch/demos/baeckerei/croissants-400.webp new file mode 100644 index 0000000..3ae59c3 Binary files /dev/null and b/public/relaunch/demos/baeckerei/croissants-400.webp differ diff --git a/public/relaunch/demos/baeckerei/roggenbrot-600.webp b/public/relaunch/demos/baeckerei/roggenbrot-600.webp new file mode 100644 index 0000000..c4faf80 Binary files /dev/null and b/public/relaunch/demos/baeckerei/roggenbrot-600.webp differ diff --git a/public/relaunch/demos/baeckerei/roggenbrot-anschnitt-400.webp b/public/relaunch/demos/baeckerei/roggenbrot-anschnitt-400.webp new file mode 100644 index 0000000..f83ab72 Binary files /dev/null and b/public/relaunch/demos/baeckerei/roggenbrot-anschnitt-400.webp differ diff --git a/public/relaunch/demos/baeckerei/zimtschnecken-400.webp b/public/relaunch/demos/baeckerei/zimtschnecken-400.webp new file mode 100644 index 0000000..293fb3d Binary files /dev/null and b/public/relaunch/demos/baeckerei/zimtschnecken-400.webp differ diff --git a/public/relaunch/demos/baeckerei/zwetschgenkuchen-520.webp b/public/relaunch/demos/baeckerei/zwetschgenkuchen-520.webp new file mode 100644 index 0000000..5cf3225 Binary files /dev/null and b/public/relaunch/demos/baeckerei/zwetschgenkuchen-520.webp differ diff --git a/public/relaunch/portal/desktop-1-projekte-1200.webp b/public/relaunch/portal/desktop-1-projekte-1200.webp deleted file mode 100644 index 213064e..0000000 Binary files a/public/relaunch/portal/desktop-1-projekte-1200.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-1-projekte-1600.webp b/public/relaunch/portal/desktop-1-projekte-1600.webp deleted file mode 100644 index 75421bd..0000000 Binary files a/public/relaunch/portal/desktop-1-projekte-1600.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-1-projekte-800.webp b/public/relaunch/portal/desktop-1-projekte-800.webp deleted file mode 100644 index cc6d514..0000000 Binary files a/public/relaunch/portal/desktop-1-projekte-800.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-3b-chat-foto-1200.webp b/public/relaunch/portal/desktop-3b-chat-foto-1200.webp deleted file mode 100644 index a52c8a3..0000000 Binary files a/public/relaunch/portal/desktop-3b-chat-foto-1200.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-3b-chat-foto-1600.webp b/public/relaunch/portal/desktop-3b-chat-foto-1600.webp deleted file mode 100644 index 54a5a4f..0000000 Binary files a/public/relaunch/portal/desktop-3b-chat-foto-1600.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-3b-chat-foto-800.webp b/public/relaunch/portal/desktop-3b-chat-foto-800.webp deleted file mode 100644 index b2bbe23..0000000 Binary files a/public/relaunch/portal/desktop-3b-chat-foto-800.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-3c-chat-leer-1600.webp b/public/relaunch/portal/desktop-3c-chat-leer-1600.webp new file mode 100644 index 0000000..ecfb478 Binary files /dev/null and b/public/relaunch/portal/desktop-3c-chat-leer-1600.webp differ diff --git a/public/relaunch/portal/desktop-3c-chat-leer-2400.webp b/public/relaunch/portal/desktop-3c-chat-leer-2400.webp new file mode 100644 index 0000000..7db58ad Binary files /dev/null and b/public/relaunch/portal/desktop-3c-chat-leer-2400.webp differ diff --git a/public/relaunch/portal/phone-1-projekte-390.webp b/public/relaunch/portal/phone-1-projekte-390.webp index 9b30a06..e06bdb5 100644 Binary files a/public/relaunch/portal/phone-1-projekte-390.webp and b/public/relaunch/portal/phone-1-projekte-390.webp differ diff --git a/public/relaunch/portal/phone-1-projekte-780.webp b/public/relaunch/portal/phone-1-projekte-780.webp index a4b2f11..f71dea1 100644 Binary files a/public/relaunch/portal/phone-1-projekte-780.webp and b/public/relaunch/portal/phone-1-projekte-780.webp differ diff --git a/public/relaunch/portal/phone-2-projekte-lang-390.webp b/public/relaunch/portal/phone-2-projekte-lang-390.webp index 26f5eb1..8c7336b 100644 Binary files a/public/relaunch/portal/phone-2-projekte-lang-390.webp and b/public/relaunch/portal/phone-2-projekte-lang-390.webp differ diff --git a/public/relaunch/portal/phone-2-projekte-lang-780.webp b/public/relaunch/portal/phone-2-projekte-lang-780.webp index 433d4ed..4eecb6a 100644 Binary files a/public/relaunch/portal/phone-2-projekte-lang-780.webp and b/public/relaunch/portal/phone-2-projekte-lang-780.webp differ diff --git a/public/relaunch/portal/phone-4b-chat-foto-390.webp b/public/relaunch/portal/phone-4b-chat-foto-390.webp deleted file mode 100644 index 7508d96..0000000 Binary files a/public/relaunch/portal/phone-4b-chat-foto-390.webp and /dev/null differ diff --git a/public/relaunch/portal/phone-4b-chat-foto-780.webp b/public/relaunch/portal/phone-4b-chat-foto-780.webp deleted file mode 100644 index 3d815d9..0000000 Binary files a/public/relaunch/portal/phone-4b-chat-foto-780.webp and /dev/null differ diff --git a/public/relaunch/portal/phone-4c-chat-leer-390.webp b/public/relaunch/portal/phone-4c-chat-leer-390.webp new file mode 100644 index 0000000..ac081ad Binary files /dev/null and b/public/relaunch/portal/phone-4c-chat-leer-390.webp differ diff --git a/public/relaunch/portal/phone-4c-chat-leer-780.webp b/public/relaunch/portal/phone-4c-chat-leer-780.webp new file mode 100644 index 0000000..abf7d24 Binary files /dev/null and b/public/relaunch/portal/phone-4c-chat-leer-780.webp differ diff --git a/scripts/relaunch-baeckerei-fotos.py b/scripts/relaunch-baeckerei-fotos.py new file mode 100644 index 0000000..a61b36b --- /dev/null +++ b/scripts/relaunch-baeckerei-fotos.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Relaunch: Fotos der erfundenen Beispiel-Website „Bäckerei Sonnenschein“ (Station Live-Vorschau und Chat-Demo). + +Alle Fotos stammen von Wikimedia Commons und stehen unter CC0 (gemeinfrei, keine Namensnennung nötig). +Es sind nur Backwaren zu sehen, keine Menschen. Quellen: docs/relaunch/assets/baeckerei/QUELLEN.md. + + Wikimedia Commons (einmalig, Zwischenspeicher docs/relaunch/assets/baeckerei/roh/, nicht im Repository) + -> public/relaunch/demos/baeckerei/-.webp + +Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-baeckerei-fotos.py +Braucht Python 3 und Pillow mit WebP-Unterstützung. +""" +from pathlib import Path +import sys +import urllib.parse +import urllib.request + +from PIL import Image + +WURZEL = Path(__file__).resolve().parent.parent +ROH = WURZEL / "docs" / "relaunch" / "assets" / "baeckerei" / "roh" +ZIEL = WURZEL / "public" / "relaunch" / "demos" / "baeckerei" +KENNUNG = {"User-Agent": "WEBklar-relaunch/1.0 (support@webklar.com)"} + +KB = 1024 +QUALITAET = 78 +MIN_QUALITAET = 50 + +# Name -> (Datei auf Commons, Ausschnitt als Anteile (links, oben, rechts, unten), Zielbreiten, Seitenverhältnis B/H, Budget je Datei) +FOTOS = { + # Startseite und Chat-Foto: dasselbe Roggenbrot (Marlene schickt es im Chat, danach steht es auf der Website) + "roggenbrot": ("Rye bread Roggenbengel 2025-02-14 13.jpg", (0.0, 0.0, 1.0, 1.0), (600,), 1.08, 58 * KB), + "roggenbrot-anschnitt": ("Cut rye bread Kartoffelkruste 2025-02-20 02.jpg", (0.12, 0.12, 0.92, 0.95), (400,), 0.8, 30 * KB), + "broetchen": ("Bread-rolls.jpg", (0.0, 0.0, 1.0, 1.0), (400,), 0.8, 30 * KB), + "croissants": ("Freshippo croissant bread.jpg", (0.05, 0.0, 0.95, 1.0), (400,), 0.8, 34 * KB), + "zimtschnecken": ("IKEA cinnamon bun.jpg", (0.05, 0.0, 0.95, 1.0), (400,), 0.8, 30 * KB), + "baguettes": ("Baguette-246424.jpg", (0.0, 0.0, 1.0, 1.0), (700,), 1.2, 40 * KB), + "zwetschgenkuchen": ("Plum cake, Ehrenbach.jpg", (0.18, 0.08, 1.0, 1.0), (520,), 1.0, 50 * KB), +} + + +def lade(datei: str) -> Path: + """Lädt das Foto einmal in 2000 px Breite von Commons (Special:FilePath) und hält es im Zwischenspeicher.""" + ROH.mkdir(parents=True, exist_ok=True) + ziel = ROH / datei.replace(" ", "_") + if not ziel.exists(): + url = "https://commons.wikimedia.org/w/index.php?title=Special:FilePath/" + urllib.parse.quote(datei) + "&width=2000" + with urllib.request.urlopen(urllib.request.Request(url, headers=KENNUNG), timeout=60) as antwort: + ziel.write_bytes(antwort.read()) + return ziel + + +def zuschneiden(bild: Image.Image, anteile: tuple, verhaeltnis: float) -> Image.Image: + """Schneidet erst den Ausschnitt aus, dann mittig auf das Seitenverhältnis.""" + b, h = bild.size + l, o, r, u = anteile + bild = bild.crop((round(l * b), round(o * h), round(r * b), round(u * h))) + b, h = bild.size + if b / h > verhaeltnis: + neu = round(h * verhaeltnis) + return bild.crop(((b - neu) // 2, 0, (b - neu) // 2 + neu, h)) + neu = round(b / verhaeltnis) + return bild.crop((0, (h - neu) // 2, b, (h - neu) // 2 + neu)) + + +def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]: + qualitaet = QUALITAET + while True: + bild.save(ziel, "WEBP", quality=qualitaet, method=6) + groesse = ziel.stat().st_size + if groesse <= budget or qualitaet - 4 < MIN_QUALITAET: + return groesse, qualitaet + qualitaet -= 4 + + +def main() -> int: + ZIEL.mkdir(parents=True, exist_ok=True) + for alt in ZIEL.glob("*.webp"): + alt.unlink() + summe = 0 + for name, (datei, anteile, breiten, verhaeltnis, budget) in FOTOS.items(): + roh = zuschneiden(Image.open(lade(datei)).convert("RGB"), anteile, verhaeltnis) + for breite in breiten: + klein = roh.resize((breite, round(breite / verhaeltnis)), Image.LANCZOS) + groesse, q = speichern(klein, ZIEL / f"{name}-{breite}.webp", budget) + summe += groesse + print(f"{name}-{breite}.webp {klein.size[0]}x{klein.size[1]} {groesse / KB:.1f} KB q{q}") + print(f"Summe: {summe / KB:.0f} KB") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/relaunch-bilder.py b/scripts/relaunch-bilder.py index 61cb218..69c8468 100644 --- a/scripts/relaunch-bilder.py +++ b/scripts/relaunch-bilder.py @@ -43,15 +43,16 @@ LANG_HOEHE = 5131 # Dateiname (ohne .webp) -> (Art, Zielbreiten) BILDER = { - # Desktop-Fenster (Roh 3024 x 1786 = MacBook-Safari-Fläche 1512 x 893 @2x) - "desktop-1-projekte": ("desktop", (800, 1200, 1600)), + # Desktop-Fenster (Roh 3024 x 1786 = MacBook-Safari-Fläche 1512 x 893 @2x); die Live-Vorschau zeigt im MacBook die + # Beispiel-Website selbst (BaeckereiSeite), kein Bild "desktop-2-fortschritt": ("desktop", (800, 1200, 1600)), - "desktop-3b-chat-foto": ("desktop", (800, 1200, 1600)), + # Chat-Demo (Station Chat): leerer Chat als Kulisse, die Nachrichten sind live; 2400 px, weil die Kamera hineinzoomt + "desktop-3c-chat-leer": ("desktop", (1600, 2400)), "desktop-4-rechnungen": ("desktop", (800, 1200, 1600)), # Handy (Roh 804 x 1748 = iPhone 16 Pro @2x, Safe Area 59/34; das lange Bild 804 x 5340, beschnitten auf 5131) "phone-1-projekte": ("handy", (390, 780)), "phone-3-projektdetails": ("handy", (390, 780)), - "phone-4b-chat-foto": ("handy", (390, 780)), + "phone-4c-chat-leer": ("handy", (390, 780)), "phone-5-rechnungen": ("handy", (390, 780)), # Funktions-Stationen (Spezifikation 2026-10-04-funktionen-demos.md): Handy-Ansicht der Funktion im Kundenportal "phone-9-termine": ("handy", (390, 780)), diff --git a/src/components/home/Kundenportal.tsx b/src/components/home/Kundenportal.tsx index 2907369..fa52b99 100644 --- a/src/components/home/Kundenportal.tsx +++ b/src/components/home/Kundenportal.tsx @@ -1,10 +1,23 @@ -import { lazy, Suspense, useEffect, useRef, useState, useSyncExternalStore, version, type ComponentType, type ReactNode, type RefObject } from "react"; +import { + lazy, + Suspense, + useEffect, + useLayoutEffect, + useRef, + useState, + useSyncExternalStore, + version, + type ComponentType, + type ReactNode, + type RefObject, +} from "react"; import WischReihe from "@/components/WischReihe"; import { DesktopFenster, HandyRahmen, RahmenBild, type RahmenBildDaten } from "@/components/GeraeteRahmen"; -import { useDemoLauf, useFeinerZeiger, useImBild } from "./demos/gemeinsam"; +import { useBewegungErlaubt, useDemoLauf, useFeinerZeiger, useImBild } from "./demos/gemeinsam"; import type { TerminZustand } from "./demos/termin/ablauf"; import type { HeatmapZustand } from "./demos/heatmap/ablauf"; import type { PinnwandZustand } from "./demos/pinnwand/ablauf"; +import type { ChatZustand } from "./demos/chat/ablauf"; import "./kundenportal.css"; /** @@ -15,6 +28,10 @@ import "./kundenportal.css"; * Die Stationen 5 bis 7 (Terminplaner, Website-Analyse, News-Pinnwand) zeigen statt eines Bildes eine * Live-Demo: auf dem MacBook die erfundene Kunden-Website, auf dem iPhone das Portal (Spezifikation * 2026-10-04-funktionen-demos). Die Demos werden per React.lazy erst geladen, wenn die Bühne ihre Station fast erreicht. + * Live sind außerdem (Wunsch des Inhabers 2026-10-04, Spezifikation 2026-10-04-vorschau-chat): + * - Live-Vorschau: im MacBook die Beispiel-Website der Bäckerei Sonnenschein, die beim Scrollen durch die Station mitläuft; + * - Chat: im MacBook der Portal-Chat, die Kamera zoomt hinein, Nachrichten ploppen auf (im iPhone im Gleichtakt). + * Der Überpunkt „Funktionen“ klappt weich auf und zu (Unterpunkte falten sich ein, „Als App“ gleitet nach, FLIP). */ const EINLEITUNG = @@ -51,6 +68,9 @@ const handyBild = (name: string, alt: string, sizes: string, hoehe = 1696): Rahm }); type DemoArt = "termin" | "heatmap" | "pinnwand"; +/** Live-Stationen außerhalb der Gruppe „Funktionen“ */ +type LiveArt = "vorschau" | "chat"; +type LadeArt = DemoArt | LiveArt; type Funktion = { id: string; @@ -64,13 +84,17 @@ type Funktion = { handy: { name: string; alt: string }; /** Der lange Screenshot, der sich im Handy-Rahmen nach oben schiebt. */ lang?: boolean; - /** Live-Demo statt Desktop-Bild: welche erfundene Kunden-Website im MacBook läuft. */ + /** Live-Demo statt Desktop-Bild: welche erfundene Kunden-Website im MacBook läuft (gehört zur Gruppe „Funktionen“). */ demo?: DemoArt; + /** Live-Station außerhalb der Gruppe: Beispiel-Website (Live-Vorschau) oder Portal-Chat mit Zoom (Chat). */ + live?: LiveArt; + /** Text der Zeile „Erfundenes Beispiel · …“ unter dem MacBook, wenn nicht die Adresse dort stehen soll. */ + beispiel?: string; /** Adresse in der Safari-Leiste (reservierte Endung .example, es ist kein echter Betrieb gemeint). */ adresse?: string; /** Beschreibung der Beispiel-Website für Screenreader (die Website selbst ist aria-hidden). */ beschreibung?: string; - /** Hinweis unter dem MacBook (Terminplaner und Pinnwand; bei der Website-Analyse steht dort die Diagnose). */ + /** Hinweis unter dem MacBook (Live-Vorschau, Chat, Terminplaner, Pinnwand; bei der Website-Analyse steht dort die Diagnose). */ hinweis?: string; }; @@ -84,7 +108,12 @@ const FUNKTIONEN: Funktion[] = [ id: "live-vorschau", titel: "Live-Vorschau", unterschrift: "Ihre Website, noch während wir daran bauen.", - desktop: { name: "desktop-1-projekte", alt: "Kundenportal: Projektübersicht mit Live-Vorschau der Website" }, + live: "vorschau", + adresse: "baeckerei-sonnenschein.project.webklar.com", + beispiel: "Bäckerei Sonnenschein", + beschreibung: + "Beispiel-Website einer Bäckerei in der Live-Vorschau: Startseite mit frischem Roggenbrot, Sortiment, Backstube, Torten und Öffnungszeiten", + hinweis: "Geschützt: Ihre Vorschau öffnet sich nur nach der Anmeldung im Kundenportal – Fremde sehen nichts.", handy: { name: "phone-1-projekte", alt: "Kundenportal auf dem Handy: Projektübersicht mit Live-Vorschau der Website" }, }, { @@ -104,8 +133,12 @@ const FUNKTIONEN: Funktion[] = [ id: "chat", titel: "Chat", unterschrift: "Frage, Foto oder Sprachnachricht direkt ans Team.", - desktop: { name: "desktop-3b-chat-foto", alt: "Kundenportal: Chat mit dem Team, im Verlauf ein Foto" }, - handy: { name: "phone-4b-chat-foto", alt: "Kundenportal auf dem Handy: Chat mit Foto" }, + live: "chat", + beispiel: "Bäckerei Sonnenschein im Kundenportal", + beschreibung: + "Chat im Kundenportal: Die Bäckerei schickt ein Foto vom neuen Roggenbrot und eine Sprachnachricht, Kenso vom WEBklar-Team antwortet", + hinweis: "Antwortet das Team, kommt zusätzlich eine E-Mail – so verpassen Sie keine Nachricht.", + handy: { name: "phone-4c-chat-leer", alt: "Kundenportal auf dem Handy: Chat mit dem WEBklar-Team" }, }, { id: "rechnungen", @@ -166,9 +199,16 @@ const FUNKTIONEN: Funktion[] = [ ]; const LETZTER = FUNKTIONEN.length - 1; -/** Stationen vor "Als App": normale Schritte und längere Demo-Schritte (bestimmen die Scrollstrecke der Hülle). */ -const ANZAHL_DEMO = FUNKTIONEN.filter((f) => f.demo).length; -const ANZAHL_NORMAL = LETZTER - ANZAHL_DEMO; +/** Art der Station: bestimmt, was nachgeladen wird und welche Hinweise unter dem MacBook stehen. */ +const ladeArt = (f: Funktion): LadeArt | undefined => f.demo ?? f.live; + +/** + * Länge jedes Schritts auf der Bühne (CSS-Variable aus kundenportal.css): normale Stationen kurz, die Demos länger + * (der Ablauf soll zu sehen sein), die Live-Vorschau am längsten, weil die Website beim Scrollen durchläuft. + * Die Summe ergibt mit der Bühnenhöhe und dem letzten Schritt die Scrollstrecke der Hülle. + */ +const strecke = (f: Funktion) => (f.live ? `var(--kp-l-${f.live})` : f.demo ? "var(--kp-l-demo)" : "var(--kp-l-normal)"); +const HUELLE_HOEHE = `calc(var(--kp-buehne-h) + ${FUNKTIONEN.slice(0, LETZTER).map(strecke).join(" + ")} + var(--kp-ende))`; /** Überpunkt der drei Funktionen in der Liste der Bühne (Wunsch des Inhabers 2026-10-04): klappt auf, sobald eine * der drei erreicht ist; die Unterpunkte zeigen nur ihren Titel. */ const GRUPPE = { titel: "Funktionen", text: "Bausteine für Ihre Website." }; @@ -187,6 +227,10 @@ const lazyMitVorladen = (laden: () => Promise<{ default: ComponentType

}> return Object.assign(Komponente, { vorladen: () => void lade().catch(() => {}) }); }; +const BaeckereiSeite = lazyMitVorladen(() => import("./demos/baeckerei/BaeckereiSeite")); +const ChatSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.ChatSteuerung }))); +const ChatSeite = lazyMitVorladen(() => import("./demos/chat/ChatSeite")); +const ChatHandy = lazyMitVorladen(() => import("./demos/chat/ChatHandy")); const TerminSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.TerminSteuerung }))); const HeatmapSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.HeatmapSteuerung }))); const PinnwandSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.PinnwandSteuerung }))); @@ -198,22 +242,31 @@ const PinnwandSeite = lazyMitVorladen(() => import("./demos/pinnwand/PinnwandSei const PinnwandHandy = lazyMitVorladen(() => import("./demos/pinnwand/PinnwandHandy")); /** Chunks je Demo; die Website-Chunks (Seite, Diagnose) und der Taktgeber laden gleichzeitig. */ -const VORLADEN: Record void)[]> = { +const VORLADEN: Record void)[]> = { + vorschau: [BaeckereiSeite.vorladen], + // ChatHandy braucht nur die Wisch-Reihe (auf der Bühne gleitet das iPhone in der Station Chat weg), ist aber winzig + chat: [ChatSteuerung.vorladen, ChatSeite.vorladen, ChatHandy.vorladen], termin: [TerminSteuerung.vorladen, TerminSeite.vorladen, TerminHandy.vorladen], heatmap: [HeatmapSteuerung.vorladen, HeatmapSeite.vorladen, HeatmapDiagnose.vorladen], pinnwand: [PinnwandSteuerung.vorladen, PinnwandSeite.vorladen, PinnwandHandy.vorladen], }; /** Hintergrund der Beispiel-Websites, solange ihr Chunk lädt (statt einer schwarzen Fläche im MacBook). */ -const PLATZHALTER: Record = { termin: "#f6f1ea", heatmap: "#141414", pinnwand: "#f7f2e6" }; -const Platzhalter = ({ art }: { art: DemoArt }) => ( +const PLATZHALTER: Record = { + vorschau: "#f7f0e4", + chat: "#0b0712", + termin: "#f6f1ea", + heatmap: "#141414", + pinnwand: "#f7f2e6", +}; +const Platzhalter = ({ art }: { art: LadeArt }) => (