diff --git a/docs/superpowers/specs/2026-10-01-relaunch-design.md b/docs/superpowers/specs/2026-10-01-relaunch-design.md index 42060f9..b4ba598 100644 --- a/docs/superpowers/specs/2026-10-01-relaunch-design.md +++ b/docs/superpowers/specs/2026-10-01-relaunch-design.md @@ -249,7 +249,7 @@ Texte in **[eckigen Klammern]** brauchen die Freigabe des Inhabers (Abschnitt 11 - Keine festgepinnte Bühne. - Stattdessen eine **Wisch-Reihe** (9.4) mit je einem Handy-Screenshot im Rahmen und der Bildunterschrift darunter. - Die letzte Karte „Als App“ zeigt `phone-2-projekte-lang`. Es schiebt sich langsam nach oben, solange die Karte sichtbar ist. -- Bei O9 = ja höchstens 7 Karten. +- Bei O9 = ja höchstens 9 Karten (Stand mit „Öffnungszeiten live“: die Funktions-Reihe hat neun Stationen). **Schrittzeile unter dem Portal** (ersetzt „So arbeiten wir“): - Text: „**1** Kennenlernen · **2** Angebot mit Preis · **3** Bauen mit Vorschau und Ihrer Freigabe · **4** Online – Hosting und Pflege auf Wunsch im Abo“ @@ -496,7 +496,7 @@ Vor jedem Löschen prüfen, dass nichts mehr die Datei importiert. - Die Fokusreihenfolge entspricht der Anzeige. - Kein `aria-live`, keine automatische Bewegung. - **Ab 900 px** zeigt jeder Abschnitt seine Desktop-Darstellung (Liste, Raster oder Bühne). Auf dem Desktop bleibt keine Wisch-Reihe übrig. -- **Grenze:** Höchstens 7 Karten je Reihe. +- **Grenze:** Höchstens 9 Karten je Reihe (Stand mit „Öffnungszeiten live“; vorher 7). ### 9.5 Bilder - **Format und Auslieferung:** diff --git a/docs/superpowers/specs/2026-10-04-funktionen-demos.md b/docs/superpowers/specs/2026-10-04-funktionen-demos.md index f1a849c..4da026f 100644 --- a/docs/superpowers/specs/2026-10-04-funktionen-demos.md +++ b/docs/superpowers/specs/2026-10-04-funktionen-demos.md @@ -130,7 +130,7 @@ reagiert auf Tippen (Zone antippen ⇒ Diagnose unter der Karte). möglichst als eigener Chunk (`React.lazy`), damit die Startseite nicht schwerer wird. - Inaktive Ebenen der Bühne bekommen `pointer-events: none`, damit nur die sichtbare Website auf die Maus reagiert. - Tests: Vitest je Demo (Rendern Desktop/Handy, statischer Endzustand ohne Bewegung, Diagnose-Logik, - keine fokussierbaren Elemente in der Website); `relaunch-kundenportal.test.tsx` auf 8 Stationen angepasst. + keine fokussierbaren Elemente in der Website); `relaunch-kundenportal.test.tsx` auf 8 Stationen angepasst (seit „Öffnungszeiten live“: 9 Stationen). `sofort-paket.test.tsx` (Scans gegen verbotene Aussagen) bleibt grün. ## 5. Offen für den Inhaber diff --git a/public/relaunch/portal/phone-12-zeiten-390.webp b/public/relaunch/portal/phone-12-zeiten-390.webp new file mode 100644 index 0000000..eafaaa0 Binary files /dev/null and b/public/relaunch/portal/phone-12-zeiten-390.webp differ diff --git a/public/relaunch/portal/phone-12-zeiten-780.webp b/public/relaunch/portal/phone-12-zeiten-780.webp new file mode 100644 index 0000000..6dca1c9 Binary files /dev/null and b/public/relaunch/portal/phone-12-zeiten-780.webp differ diff --git a/src/components/home/Kundenportal.tsx b/src/components/home/Kundenportal.tsx index 9c44027..86cbf04 100644 --- a/src/components/home/Kundenportal.tsx +++ b/src/components/home/Kundenportal.tsx @@ -18,6 +18,7 @@ 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 type { ZeitenZustand } from "./demos/zeiten/ablauf"; import "./kundenportal.css"; /** @@ -25,7 +26,7 @@ import "./kundenportal.css"; * Ab 900 px: festgepinnte Bühne mit Funktionsliste links und Desktop-Fenster plus Handy-Rahmen rechts. * Darunter (unter 900 px): keine Bühne, sondern eine Wisch-Reihe mit je einem Handy-Screenshot. * Danach die Schrittzeile (ersetzt "So arbeiten wir"). - * Die Stationen 5 bis 7 (Terminplaner, Website-Analyse, News-Pinnwand) zeigen statt eines Bildes eine + * Die Stationen 5 bis 8 (Terminplaner, Website-Analyse, News-Pinnwand, Öffnungszeiten live) 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): @@ -67,7 +68,7 @@ const handyBild = (name: string, alt: string, sizes: string, hoehe = 1696): Rahm alt, }); -type DemoArt = "termin" | "heatmap" | "pinnwand"; +type DemoArt = "termin" | "heatmap" | "pinnwand" | "zeiten"; /** Live-Stationen außerhalb der Gruppe „Funktionen“ */ type LiveArt = "vorschau" | "chat"; type LadeArt = DemoArt | LiveArt; @@ -94,9 +95,9 @@ type Funktion = { adresse?: string; /** Beschreibung der Beispiel-Website für Screenreader (die Website selbst ist aria-hidden). */ beschreibung?: string; - /** Hinweis unter dem MacBook (Live-Vorschau, Chat, Terminplaner, 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, bei Öffnungszeiten live der Uhr-Regler). */ hinweis?: string; - /** Noch nicht für Kunden buchbar: Marke „Bald verfügbar“ am Titel (Terminplaner läuft bisher nur im Pilot, Stand 04.10.2026). */ + /** Noch nicht für Kunden buchbar: Marke „Bald verfügbar“ am Titel (Terminplaner läuft bisher nur im Pilot, Stand 04.10.2026; Öffnungszeiten live bis zum Portal-Livegang). */ bald?: boolean; }; @@ -104,6 +105,8 @@ const BaldMarke = ({ f }: { f: Funktion }) => (f.bald ? 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 }))); +const ZeitenSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.ZeitenSteuerung }))); const TerminSeite = lazyMitVorladen(() => import("./demos/termin/TerminSeite")); const TerminHandy = lazyMitVorladen(() => import("./demos/termin/TerminHandy")); const HeatmapSeite = lazyMitVorladen(() => import("./demos/heatmap/HeatmapSeite")); const HeatmapDiagnose = lazyMitVorladen(() => import("./demos/heatmap/HeatmapDiagnose")); const PinnwandSeite = lazyMitVorladen(() => import("./demos/pinnwand/PinnwandSeite")); const PinnwandHandy = lazyMitVorladen(() => import("./demos/pinnwand/PinnwandHandy")); +const ZeitenSeite = lazyMitVorladen(() => import("./demos/zeiten/ZeitenSeite")); +const ZeitenHandy = lazyMitVorladen(() => import("./demos/zeiten/ZeitenHandy")); +const ZeitenRegler = lazyMitVorladen(() => import("./demos/zeiten/ZeitenRegler")); /** Chunks je Demo; die Website-Chunks (Seite, Diagnose) und der Taktgeber laden gleichzeitig. */ const VORLADEN: Record void)[]> = { @@ -254,6 +271,7 @@ const VORLADEN: Record void)[]> = { termin: [TerminSteuerung.vorladen, TerminSeite.vorladen, TerminHandy.vorladen], heatmap: [HeatmapSteuerung.vorladen, HeatmapSeite.vorladen, HeatmapDiagnose.vorladen], pinnwand: [PinnwandSteuerung.vorladen, PinnwandSeite.vorladen, PinnwandHandy.vorladen], + zeiten: [ZeitenSteuerung.vorladen, ZeitenSeite.vorladen, ZeitenHandy.vorladen, ZeitenRegler.vorladen], }; /** Hintergrund der Beispiel-Websites, solange ihr Chunk lädt (statt einer schwarzen Fläche im MacBook). */ @@ -263,6 +281,7 @@ const PLATZHALTER: Record = { termin: "#f6f1ea", heatmap: "#141414", pinnwand: "#f7f2e6", + zeiten: "#f7f0e4", }; const Platzhalter = ({ art }: { art: LadeArt }) => ( @@ -664,6 +694,19 @@ const Buehne = () => { ) : null} + {/* Öffnungszeiten live: der Uhr-Regler (echter Schieberegler), nur bei aktiver Station sichtbar und bedienbar */} +
+ {demoEingehaengt("zeiten") ? ( + + + + ) : null} +
{FUNKTIONEN.filter((f) => ladeArt(f) && f.hinweis).map((f) => (

{ ); }; +/** Öffnungszeiten-Karte: die Website im Handy, darunter der Uhr-Regler (Ziehen ändert Status und Uhrzeit auf der Website). */ +const ZeitenKarte = ({ f }: { f: Funktion }) => { + const k = useKarte("zeiten"); + const [z, setZ] = useState(null); + return ( + + {k.gesehen ? : null} + + } + > + {k.gesehen ? : null} + {k.gesehen ? : null} + + ); +}; + /** Heatmap-Karte: Tippen auf eine Zone wählt sie, darunter erscheint die kompakte Diagnose. */ const HeatmapKarte = ({ f }: { f: Funktion }) => { const k = useKarte("heatmap"); @@ -825,13 +888,14 @@ const HeatmapKarte = ({ f }: { f: Funktion }) => { const HandyReihe = () => (

- + {FUNKTIONEN.map((f) => { if (f.lang) return ; if (f.live === "chat") return ; if (f.demo === "termin") return ; if (f.demo === "heatmap") return ; if (f.demo === "pinnwand") return ; + if (f.demo === "zeiten") return ; return (
diff --git a/src/components/home/demos/steuerung.tsx b/src/components/home/demos/steuerung.tsx index 14b6efc..a3ba49f 100644 --- a/src/components/home/demos/steuerung.tsx +++ b/src/components/home/demos/steuerung.tsx @@ -3,9 +3,10 @@ import { useTerminAblauf, type TerminZustand } from "./termin/ablauf"; import { useHeatmapAblauf, type HeatmapZustand } from "./heatmap/ablauf"; import { usePinnwandAblauf, type PinnwandZustand } from "./pinnwand/ablauf"; import { useChatAblauf, type ChatZustand } from "./chat/ablauf"; +import { useZeitenAblauf, type ZeitenZustand } from "./zeiten/ablauf"; /** - * Taktgeber der Demos (drei Funktionen und der Chat) als unsichtbare Bauteile (rendern nichts, melden nur ihren Zustand). + * Taktgeber der Demos (vier Funktionen und der Chat) als unsichtbare Bauteile (rendern nichts, melden nur ihren Zustand). * Sie liegen in einem eigenen Chunk, damit die Startseite die Ablauf-Logik erst lädt, wenn die Kundenportal-Bühne * eine Demo-Station fast erreicht (Spezifikation 4). Die Bühne hält den gemeldeten Zustand und gibt denselben * Zustand an Website, Handy-Ebene und Diagnose. @@ -51,3 +52,19 @@ export const HeatmapSteuerung = memo(function HeatmapSteuerung({ useEffect(() => onZustand(zustand), [zustand, onZustand]); return null; }); + +/** `aktiv`: Station ist die aktive; sonst geht eine Bedienung des Uhr-Reglers zurück an den Ablauf (beim nächsten Mal von vorn). */ +export const ZeitenSteuerung = memo(function ZeitenSteuerung({ + laeuft, + statisch, + aktiv, + onZustand, +}: Props & { aktiv: boolean }) { + const zustand = useZeitenAblauf(laeuft, statisch); + const { setzeUhr } = zustand; + useEffect(() => { + if (!aktiv) setzeUhr(null); + }, [aktiv, setzeUhr]); + useEffect(() => onZustand(zustand), [zustand, onZustand]); + return null; +}); diff --git a/src/components/home/demos/zeiten/ZeitenHandy.tsx b/src/components/home/demos/zeiten/ZeitenHandy.tsx new file mode 100644 index 0000000..46a0392 --- /dev/null +++ b/src/components/home/demos/zeiten/ZeitenHandy.tsx @@ -0,0 +1,31 @@ +import { DemoZeiger } from "../gemeinsam"; +import { HINWEIS_ANLEGEN_PUNKT, type ZeitenZustand } from "./ablauf"; + +/** Wo der Finger wartet, bevor er zum Knopf gleitet (unten rechts, neben dem Home-Balken). */ +const RUHE = { x: 318, y: 842 }; + +// Inline, damit die Ebene die Fläche genau überdeckt und nichts anklickt, auch ohne geladenes CSS. +const EBENE = { position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" } as const; + +type Props = { + zustand: ZeitenZustand; + /** Mitte des Knopfes „Hinweis-Leiste anlegen“ im Portal-Bild; Standard: HINWEIS_ANLEGEN_PUNKT. */ + punkt?: { x: number; y: number }; +}; + +/** + * Live-Ebene über dem Portal-Screenshot im iPhone der Bühne (402 x 874, transparent, pointer-events none): + * in Phase „tippen“ gleitet ein Finger auf „Hinweis-Leiste anlegen“ und tippt; danach gleitet auf der Website die Leiste ein. + * Davor (Uhr läuft) wartet der Finger unsichtbar in der Ruhelage, damit er beim Start von dort zum Knopf gleitet. + */ +const ZeitenHandy = ({ zustand, punkt = HINWEIS_ANLEGEN_PUNKT }: Props) => { + const amKnopf = zustand.phase === "tippen" || zustand.phase === "leiste"; + const ziel = amKnopf ? punkt : RUHE; + return ( + + ); +}; + +export default ZeitenHandy; diff --git a/src/components/home/demos/zeiten/ZeitenRegler.tsx b/src/components/home/demos/zeiten/ZeitenRegler.tsx new file mode 100644 index 0000000..4d0c05f --- /dev/null +++ b/src/components/home/demos/zeiten/ZeitenRegler.tsx @@ -0,0 +1,48 @@ +import { useId } from "react"; +import { ZEITEN_START, ZEIT_MAX, ZEIT_MIN, ZEIT_SCHRITT, formatUhr, type ZeitenZustand } from "./ablauf"; +import "./zeiten.css"; + +/** + * Der Uhr-Regler unter dem MacBook (Station „Öffnungszeiten live“): ein echter Schieberegler (input type=range) mit Label, + * der Uhrzeit und dem Statussatz der Bäckerei zu dieser Uhrzeit. Wer ihn anfasst, hält den Automatiklauf an; + * die Website zeigt dann den Status der gewählten Uhrzeit. Normaler Seitenmaßstab mit den Seitentoken, hell und dunkel. + * `kompakt` ist für die Handy-Karte (schmal, bis 320 px). + * Der Statussatz meldet sich Screenreadern (aria-live) nur nach einer Bedienung, nicht beim Automatiklauf. + */ + +type Props = { zustand?: ZeitenZustand; kompakt?: boolean }; + +const ZeitenRegler = ({ zustand = ZEITEN_START, kompakt = false }: Props) => { + const id = useId(); + const { uhr, status, bedient } = zustand; + return ( +
+
+ + +
+ zustand.setzeUhr(Number(e.target.value))} + /> +

+