Relaunch: Station Öffnungszeiten live

Vierter Eintrag der Funktionen-Gruppe (nach der News-Pinnwand, „Bald verfügbar“, ohne Preis):
- Website: Bäckerei Sonnenschein mit großer Status-Pille und Hinweis-Leiste „Betriebsurlaub 12.–26. Oktober“
  (gleitet per transform ein, der Inhalt rückt nach), Desktop und Handy.
- Handy-Ebene: Finger tippt „Hinweis-Leiste anlegen“ (HINWEIS_PUNKT, Platzhalter bis zur Aufnahme).
- Uhr-Regler: echter Schieberegler unter dem MacBook (inaktiv: aria-hidden + inert) und in der Handy-Karte.
- ZeitenSteuerung (setzeUhr(null) beim Verlassen), lazy-Chunks, Kundenportal importiert aus ablauf.ts nur Typen.
- Pins in relaunch-kundenportal.test.tsx auf neun Stationen angepasst (Als App = Marke 8), Spezifikations-Zähler mitgezogen.

ACHTUNG: public/relaunch/portal/phone-12-zeiten-{390,780}.webp sind vorläufig Kopien von phone-9-termine
(damit Build und Tests laufen); Task 36 ersetzt sie durch die echte Aufnahme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-05 08:05:49 +00:00
1 parent f7147fcd30
commit a23f2e52c2
14 files changed
+1234 -56

No files matched your search

+69 -5
View File
@@ -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 ? <span className="kp-bald
/** Mitte des Knopfes "Aufhängen" im Portal-Screenshot der News-Pinnwand (Designpunkte der iPhone-Fläche 402 x 874). */
const AUFHAENGEN_PUNKT = { x: 201, y: 613 };
/** Mitte des Knopfes "Hinweis-Leiste anlegen" im Portal-Screenshot der Station Öffnungszeiten live (Platzhalter bis zur Aufnahme phone-12-zeiten). */
const HINWEIS_PUNKT = { x: 201, y: 740 };
// Reihenfolge und Texte laut Spezifikation 7.2. Platzhalter des Portals ("kommt bald") werden nicht beworben,
// weitere Ansichten nur nach Freigabe des Inhabers (O9 = nein).
@@ -191,6 +194,16 @@ const FUNKTIONEN: Funktion[] = [
alt: "Kundenportal von WEBklar auf dem Handy: Ein neuer Zettel für die News-Pinnwand mit Überschrift und Text ist ausgefüllt, darunter der Knopf zum Aufhängen auf der Website.",
},
},
{
id: "oeffnungszeiten",
titel: "Öffnungszeiten live",
demo: "zeiten",
bald: true,
adresse: "baeckerei-sonnenschein.example",
beschreibung:
"Beispiel-Website einer Bäckerei mit Öffnungszeiten: Der Status „Jetzt geöffnet“ folgt der Uhrzeit, danach kündigt eine Hinweis-Leiste den Betriebsurlaub an",
handy: { name: "phone-12-zeiten", alt: "Öffnungszeiten im Kundenportal bearbeiten" },
},
{
id: "als-app",
titel: "Als App",
@@ -239,12 +252,16 @@ 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 })));
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<LadeArt, (() => void)[]> = {
@@ -254,6 +271,7 @@ const VORLADEN: Record<LadeArt, (() => 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<LadeArt, string> = {
termin: "#f6f1ea",
heatmap: "#141414",
pinnwand: "#f7f2e6",
zeiten: "#f7f0e4",
};
const Platzhalter = ({ art }: { art: LadeArt }) => (
<div className="kp-platzhalter" style={{ background: PLATZHALTER[art] }} aria-hidden="true" />
@@ -331,6 +350,7 @@ const Buehne = () => {
const [termin, setTermin] = useState<TerminZustand | null>(null);
const [heat, setHeat] = useState<HeatmapZustand | null>(null);
const [pinnwand, setPinnwand] = useState<PinnwandZustand | null>(null);
const [zeiten, setZeiten] = useState<ZeitenZustand | null>(null);
useEffect(() => {
if (typeof IntersectionObserver === "undefined") return;
@@ -488,6 +508,14 @@ const Buehne = () => {
{demoEingehaengt("pinnwand") ? (
<PinnwandSteuerung laeuft={lauf.laeuft && aktivDemo === "pinnwand"} statisch={lauf.statisch} onZustand={setPinnwand} />
) : null}
{demoEingehaengt("zeiten") ? (
<ZeitenSteuerung
laeuft={lauf.laeuft && aktivDemo === "zeiten"}
statisch={lauf.statisch}
aktiv={aktivDemo === "zeiten"}
onZustand={setZeiten}
/>
) : null}
</Suspense>
<div className="kp-spur" aria-hidden="true">
@@ -596,6 +624,7 @@ const Buehne = () => {
{f.demo === "termin" ? <TerminSeite zustand={termin ?? undefined} variante="desktop" /> : null}
{f.demo === "heatmap" ? <HeatmapSeite zustand={heat ?? undefined} variante="desktop" /> : null}
{f.demo === "pinnwand" ? <PinnwandSeite zustand={pinnwand ?? undefined} variante="desktop" /> : null}
{f.demo === "zeiten" ? <ZeitenSeite zustand={zeiten ?? undefined} variante="desktop" /> : null}
</Suspense>
) : null}
</div>
@@ -625,6 +654,7 @@ const Buehne = () => {
<Suspense fallback={null}>
{f.demo === "termin" && termin ? <TerminHandy zustand={termin} /> : null}
{f.demo === "pinnwand" && pinnwand ? <PinnwandHandy zustand={pinnwand} punkt={AUFHAENGEN_PUNKT} /> : null}
{f.demo === "zeiten" && zeiten ? <ZeitenHandy zustand={zeiten} punkt={HINWEIS_PUNKT} /> : null}
</Suspense>
) : null}
</div>
@@ -664,6 +694,19 @@ const Buehne = () => {
</Suspense>
) : null}
</div>
{/* Öffnungszeiten live: der Uhr-Regler (echter Schieberegler), nur bei aktiver Station sichtbar und bedienbar */}
<div
className="kp-regler"
data-an={aktivDemo === "zeiten" ? "ja" : "nein"}
aria-hidden={aktivDemo !== "zeiten" || undefined}
{...(aktivDemo !== "zeiten" ? INERT : null)}
>
{demoEingehaengt("zeiten") ? (
<Suspense fallback={null}>
<ZeitenRegler zustand={zeiten ?? undefined} />
</Suspense>
) : null}
</div>
{FUNKTIONEN.filter((f) => ladeArt(f) && f.hinweis).map((f) => (
<p
key={f.id}
@@ -803,6 +846,26 @@ const PinnwandKarte = ({ f }: { f: Funktion }) => {
);
};
/** Ö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<ZeitenZustand | null>(null);
return (
<DemoKarteRahmen
f={f}
geraetRef={k.ref}
nachher={
<div className="kp-karte__regler">
<Suspense fallback={null}>{k.gesehen ? <ZeitenRegler zustand={z ?? undefined} kompakt /> : null}</Suspense>
</div>
}
>
{k.gesehen ? <ZeitenSteuerung laeuft={k.laeuft} statisch={k.statisch} aktiv onZustand={setZ} /> : null}
{k.gesehen ? <ZeitenSeite zustand={z ?? undefined} variante="handy" /> : null}
</DemoKarteRahmen>
);
};
/** 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 = () => (
<div className="kp-reihe">
<WischReihe ariaLabel="Acht Funktionen des Kundenportals, seitlich wischen">
<WischReihe ariaLabel="Neun Funktionen des Kundenportals, seitlich wischen">
{FUNKTIONEN.map((f) => {
if (f.lang) return <LangeKarte key={f.id} f={f} />;
if (f.live === "chat") return <ChatKarte key={f.id} f={f} />;
if (f.demo === "termin") return <TerminKarte key={f.id} f={f} />;
if (f.demo === "heatmap") return <HeatmapKarte key={f.id} f={f} />;
if (f.demo === "pinnwand") return <PinnwandKarte key={f.id} f={f} />;
if (f.demo === "zeiten") return <ZeitenKarte key={f.id} f={f} />;
return (
<div key={f.id} className="kp-karte">
<HandyRahmen className="kp-karte__handy" bild={handyBild(f.handy.name, f.handy.alt, "250px")} />
+18 -1
View File
@@ -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<ZeitenZustand> & { aktiv: boolean }) {
const zustand = useZeitenAblauf(laeuft, statisch);
const { setzeUhr } = zustand;
useEffect(() => {
if (!aktiv) setzeUhr(null);
}, [aktiv, setzeUhr]);
useEffect(() => onZustand(zustand), [zustand, onZustand]);
return null;
});
@@ -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 (
<div style={EBENE} aria-hidden="true" data-phase={zustand.phase}>
<DemoZeiger x={ziel.x} y={ziel.y} art="finger" klick={zustand.klick} sichtbar={zustand.phase === "tippen"} groesse={1.5} />
</div>
);
};
export default ZeitenHandy;
@@ -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 (
<div className={`zr${kompakt ? " zr--kompakt" : ""}`} data-bedient={bedient ? "ja" : "nein"}>
<div className="zr__kopf">
<label className="zr__titel" htmlFor={id}>
Uhrzeit testen
</label>
<span className="zr__uhr" aria-hidden="true">
{formatUhr(uhr)}
{" "}Uhr
</span>
</div>
<input
id={id}
className="zr__regler"
type="range"
min={ZEIT_MIN}
max={ZEIT_MAX}
step={ZEIT_SCHRITT}
value={uhr}
aria-valuetext={`${formatUhr(uhr)} Uhr, ${status.kurz}`}
onChange={(e) => zustand.setzeUhr(Number(e.target.value))}
/>
<p className="zr__status" data-offen={status.offen ? "ja" : "nein"} aria-live={bedient ? "polite" : "off"} aria-atomic="true">
<i className="zr__punkt" aria-hidden="true" />
{status.text}
</p>
</div>
);
};
export default ZeitenRegler;
@@ -0,0 +1,138 @@
import type { DemoVariante } from "../gemeinsam";
import { Logo, Pfeil, Stempel } from "../baeckerei/teile";
import { HINWEIS_TEXT, ZEITEN_START, type ZeitenZustand } from "./ablauf";
import "../baeckerei/baeckerei.css";
import "./zeiten.css";
/**
* Die Beispiel-Website der Station „Öffnungszeiten live“: wieder die erfundene „Bäckerei Sonnenschein“ (Spezifikation 10),
* hier mit großer Status-Pille („Jetzt geöffnet · schließt um 18 Uhr“) und der Hinweis-Leiste „Betriebsurlaub 12.–26. Oktober“,
* die oben einschiebt, sobald der Inhaber sie im Portal anlegt (Phase „leiste“; die Seite rückt per transform nach).
* Demo-Datum: Montag, 5. Oktober 2026 (Mo bis Fr 6 bis 18 Uhr), die Uhrzeit kommt aus dem Zustand.
* Desktop = 1512 x 893 (MacBook-Fläche), Handy = 402 x 874 (oben 59 / unten 34 frei für Statusleiste und Home-Balken).
* Eigenes Wurzelelement ohne .bk-inhalt: die Seite läuft nicht mit dem Scrollfortschritt der Live-Vorschau mit.
* Die Website ist reine Kulisse: aria-hidden, keine fokussierbaren Elemente (div/span statt a/button).
*/
const FOTO = "/relaunch/demos/baeckerei/roggenbrot-600.webp";
/** Wie in der Live-Vorschau: Montag bis Freitag 6 bis 18 Uhr; der Demo-Tag ist ein Montag. */
const ZEITEN = [
{ tage: "Mo – Fr", zeit: "6:00 – 18:00 Uhr", heute: true },
{ tage: "Sa", zeit: "6:00 – 13:00 Uhr" },
{ tage: "So", zeit: "7:00 – 11:00 Uhr" },
];
const MENUE = ["Sortiment", "Backstube", "Café", "Torten", "Kontakt"];
const BANDWOERTER = ["Roggenbrot", "Dinkelvollkorn", "Buttercroissant", "Laugenbrezel", "Zimtschnecke", "Zwetschgenkuchen"];
const Karte = () => (
<div className="zt-karte">
<span className="bk-dach">Öffnungszeiten</span>
{ZEITEN.map((z) => (
<div key={z.tage} className={`zt-zeile${z.heute ? " is-heute" : ""}`}>
<span>{z.tage}</span>
<span>{z.zeit}</span>
</div>
))}
</div>
);
/** `zustand` fehlt, solange der Taktgeber noch lädt: dann steht die Website im Anfangszustand da. */
type Props = { zustand?: ZeitenZustand; variante: DemoVariante };
/** Die Website auf der MacBook-Fläche (desktop) oder in der Handy-Fassung (handy). */
const ZeitenSeite = ({ zustand = ZEITEN_START, variante }: Props) => {
const desktop = variante === "desktop";
const { status } = zustand;
return (
<div
className="bk-seite zt-seite"
lang="de"
aria-hidden="true"
data-variante={variante}
data-phase={zustand.phase}
data-laeuft={zustand.laeuft ? "ja" : "nein"}
>
<div className="zt-leiste">
<i className="zt-leiste__punkt" />
{HINWEIS_TEXT}
</div>
<div className="zt-inhalt">
<div className="bk-kopf zt-kopf">
<div className="bk-logo">
<Logo />
<span className="bk-logo__text">
<span className="bk-logo__name">Sonnenschein</span>
<span className="bk-logo__zusatz">Bäckerei · seit 1987</span>
</span>
</div>
{desktop ? (
<>
<div className="bk-menue">
{MENUE.map((m) => (
<span key={m}>{m}</span>
))}
</div>
<span className="bk-knopf bk-knopf--dunkel bk-knopf--klein">Vorbestellen</span>
</>
) : (
<span className="zt-burger">
<i />
<i />
<i />
</span>
)}
</div>
<div className="zt-held">
<div className="zt-held__text">
<div className="zt-pille" data-offen={status.offen ? "ja" : "nein"}>
<i className="zt-pille__punkt" />
{/* der Text wechselt nur, wenn sich der Status ändert; mit Bewegung blendet er dann neu ein */}
<span key={status.text} className="zt-pille__text">
{status.text}
</span>
</div>
<div className="bk-held__titel zt-titel">
Frisch gebacken.
<br />
<em>Jeden Morgen.</em>
</div>
<p className="bk-held__lead">
Brot, Brötchen und Kuchen aus unserer Backstube in der Altstadt – mit Sauerteig, viel Zeit und Mehl aus der Region.
</p>
{desktop ? (
<div className="bk-held__knoepfe">
<span className="bk-knopf bk-knopf--dunkel">
Sortiment ansehen <Pfeil />
</span>
<span className="bk-knopf bk-knopf--rand">Torte bestellen</span>
</div>
) : null}
</div>
{desktop ? (
<div className="bk-held__bild">
<img src={FOTO} width={600} height={556} alt="" decoding="async" />
<Karte />
<Stempel />
</div>
) : (
<Karte />
)}
</div>
{desktop ? (
<div className="bk-band">
{BANDWOERTER.map((w) => (
<span key={w}>{w}</span>
))}
</div>
) : null}
</div>
</div>
);
};
export default ZeitenSeite;
+341
View File
@@ -0,0 +1,341 @@
/*
* Station „Öffnungszeiten live“: Beispiel-Website der Bäckerei Sonnenschein mit großer Status-Pille (.zt-*) und der Uhr-Regler
* unter dem MacBook (.zr-*). Grundlage ist baeckerei.css (.bk-seite mit den Variablen --bk-*, Logo, Menü, Knöpfe); die Seite hat
* ein eigenes Wurzelelement ohne .bk-inhalt, sonst liefe sie mit --kp-v (Scrollfortschritt der Live-Vorschau) davon.
* Desktop 1512 x 893, Handy 402 x 874 (oben 59 / unten 34 frei).
* Im MacBook liegt das iPhone über der Fläche ab etwa x 1150 und y 410: dort steht nichts Wichtiges (Pille und Öffnungszeiten
* links davon, die Hinweis-Leiste oben, oberhalb von y 410). Größen in Designpunkten: das MacBook verkleinert auf etwa 46 %,
* darum Pille ≥ 30 und Leiste ≥ 24.
* Grundzustand statisch; Bewegung nur transform/opacity hinter prefers-reduced-motion: no-preference.
*/
.zt-seite {
--zt-leiste-h: 64px;
--zt-offen: #3f5e2c;
--zt-offen-punkt: #4c9a3c;
--zt-offen-ring: #dcebd3;
--zt-zu: #8a3a12;
--zt-zu-punkt: #c4622d;
--zt-zu-ring: #f4dccb;
}
/* ── Hinweis-Leiste: gleitet oben ein, der Inhalt rückt per transform nach ─────────────────────────── */
.zt-leiste {
position: absolute;
left: 0;
top: 0;
right: 0;
height: 64px;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
background: var(--bk-tinte);
color: #f6ead6;
font-size: 26px;
font-weight: 600;
line-height: 1.2;
white-space: nowrap;
opacity: 0;
transform: translateY(-100%);
}
.zt-leiste__punkt {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--bk-sonne);
box-shadow: 0 0 0 5px rgb(242 181 68 / 0.28);
}
.zt-inhalt {
position: absolute;
inset: 0;
}
.zt-seite[data-phase="leiste"] .zt-leiste {
opacity: 1;
transform: none;
}
.zt-seite[data-phase="leiste"] .zt-inhalt {
transform: translateY(var(--zt-leiste-h));
}
/* ── Start: Pille, Titel, Öffnungszeiten ──────────────────────────────────────────────────────────── */
.zt-held {
display: grid;
grid-template-columns: 1fr 560px;
gap: 56px;
align-items: center;
height: 640px;
padding: 0 88px;
}
.zt-held__text {
min-width: 0;
}
.zt-pille {
display: inline-flex;
align-items: center;
gap: 16px;
padding: 12px 28px 12px 22px;
border-radius: 999px;
background: var(--bk-papier);
border: 2px solid var(--bk-linie);
font-size: 30px;
font-weight: 600;
line-height: 1.25;
white-space: nowrap;
color: var(--zt-offen);
}
.zt-pille__punkt {
flex: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--zt-offen-punkt);
box-shadow: 0 0 0 6px var(--zt-offen-ring);
}
.zt-pille[data-offen="nein"] {
color: var(--zt-zu);
}
.zt-pille[data-offen="nein"] .zt-pille__punkt {
background: var(--zt-zu-punkt);
box-shadow: 0 0 0 6px var(--zt-zu-ring);
}
.zt-titel {
margin-top: 30px;
}
.zt-karte {
position: absolute;
left: -56px;
bottom: 36px;
width: 300px;
padding: 18px 22px 16px;
border-radius: 22px;
background: var(--bk-papier);
box-shadow: 0 24px 50px -24px rgb(60 30 5 / 0.45);
}
.zt-karte .bk-dach {
margin-bottom: 8px;
}
.zt-zeile {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 7px 10px;
margin: 0 -10px;
border-radius: 10px;
font-size: 18px;
color: var(--bk-tinte-leise);
}
.zt-zeile.is-heute {
background: var(--bk-sand);
color: var(--bk-tinte);
font-weight: 600;
}
/* ── Handy (402 x 874): Statusleiste oben 59 frei, Leiste darunter ─────────────────────────────────── */
.zt-seite[data-variante="handy"] {
--zt-leiste-h: 52px;
}
.zt-seite[data-variante="handy"] .zt-leiste {
top: 59px;
height: 52px;
gap: 10px;
font-size: 17px;
}
.zt-seite[data-variante="handy"] .zt-leiste__punkt {
width: 9px;
height: 9px;
box-shadow: 0 0 0 4px rgb(242 181 68 / 0.28);
}
/* die oberen 59 px überdeckt die Statusleiste des iPhones (heller Text): ein dunkler Streifen macht Uhrzeit und Symbole lesbar */
.zt-seite[data-variante="handy"]::before {
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
height: 59px;
z-index: 4;
background: var(--bk-tinte);
}
.zt-seite[data-variante="handy"] .zt-inhalt {
top: 59px;
}
.zt-seite[data-variante="handy"] .zt-kopf {
height: 64px;
padding: 0 20px;
justify-content: space-between;
}
.zt-seite[data-variante="handy"] .bk-logo {
gap: 10px;
}
.zt-seite[data-variante="handy"] .bk-logo__zeichen {
width: 38px;
height: 38px;
}
.zt-seite[data-variante="handy"] .bk-logo__name {
font-size: 27px;
}
.zt-seite[data-variante="handy"] .bk-logo__zusatz {
margin-top: 3px;
font-size: 9.5px;
}
.zt-burger {
display: flex;
flex-direction: column;
gap: 5px;
width: 26px;
}
.zt-burger i {
display: block;
height: 2.5px;
border-radius: 2px;
background: var(--bk-tinte);
}
.zt-seite[data-variante="handy"] .zt-held {
display: block;
height: auto;
padding: 22px 20px 0;
}
.zt-seite[data-variante="handy"] .zt-pille {
gap: 11px;
max-width: 100%;
padding: 9px 18px 9px 15px;
border-width: 1.5px;
font-size: 17px;
line-height: 1.25;
white-space: normal;
}
.zt-seite[data-variante="handy"] .zt-pille__punkt {
width: 11px;
height: 11px;
box-shadow: 0 0 0 4px var(--zt-offen-ring);
}
.zt-seite[data-variante="handy"] .zt-pille[data-offen="nein"] .zt-pille__punkt {
box-shadow: 0 0 0 4px var(--zt-zu-ring);
}
.zt-seite[data-variante="handy"] .zt-titel {
margin: 22px 0 14px;
font-size: 62px;
}
.zt-seite[data-variante="handy"] .bk-held__lead {
font-size: 17px;
line-height: 1.5;
}
.zt-seite[data-variante="handy"] .zt-karte {
position: static;
width: auto;
margin-top: 26px;
border: 1px solid var(--bk-linie);
box-shadow: none;
}
.zt-seite[data-variante="handy"] .zt-zeile {
font-size: 16px;
}
/* ── Uhr-Regler unter dem MacBook (normaler Seitenmaßstab, Seitentoken, hell und dunkel) ───────────── */
.zr {
box-sizing: border-box;
width: 100%;
max-width: 560px;
padding: 10px 16px 12px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--bg-alt);
color: var(--fg);
font-size: 14px;
line-height: 1.35;
text-align: left;
}
.zr *,
.zr *::before,
.zr *::after {
box-sizing: border-box;
}
.zr__kopf {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.zr__titel {
font-size: 14px;
font-weight: 600;
color: var(--fg);
}
.zr__uhr {
font-size: 20px;
font-weight: 600;
line-height: 1.2;
font-variant-numeric: tabular-nums;
color: var(--fg);
}
.zr__regler {
display: block;
width: 100%;
height: 44px;
margin: 0;
padding: 0;
background: transparent;
accent-color: var(--accent-text);
touch-action: pan-y;
cursor: pointer;
}
.zr__regler:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
border-radius: 8px;
}
.zr__status {
display: flex;
align-items: center;
gap: 9px;
margin: 0;
min-height: 20px;
font-weight: 600;
color: var(--fg);
}
.zr__punkt {
flex: none;
width: 10px;
height: 10px;
border-radius: 50%;
background: #3d9a4a;
}
.zr__status[data-offen="nein"] .zr__punkt {
background: #c4622d;
}
.zr--kompakt {
max-width: 320px;
padding: 10px 14px 12px;
}
/* ── Bewegung (nur transform und opacity, nur ohne reduzierte Bewegung) ────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
.zt-leiste {
transition:
transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1),
opacity 400ms ease;
}
.zt-inhalt {
transition: transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.zt-pille__text {
animation: zt-wechsel 420ms ease-out both;
}
}
@keyframes zt-wechsel {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
+10 -2
View File
@@ -164,7 +164,7 @@
padding: 4px 0;
}
}
/* sehr niedrige Fenster: nur die aktive Funktion zeigt ihre Unterschrift (acht Stationen brauchen sonst mehr Platz) */
/* sehr niedrige Fenster: nur die aktive Funktion zeigt ihre Unterschrift (neun Stationen brauchen sonst mehr Platz) */
@media (max-height: 720px) {
/* Oberkante bleibt stehen, wenn die Unterschriften wechseln (sonst würde die zentrierte Liste springen) */
.kp-liste {
@@ -368,16 +368,22 @@
display: grid;
}
.kp-diagnose,
.kp-regler,
.kp-hinweis {
grid-area: 1 / 1;
opacity: 0;
pointer-events: none;
}
.kp-diagnose[data-an="ja"],
.kp-regler[data-an="ja"],
.kp-hinweis[data-an="ja"] {
opacity: 1;
pointer-events: auto;
}
/* Der Regler (Öffnungszeiten live) ist nie höher als die Diagnose, damit die Zelle beim Wechsel nicht springt */
.kp-regler {
align-self: start;
}
/* Hinweiskarte im Stil der Diagnose (gleiche Linie, gleiches Padding) */
.kp-hinweis {
align-self: start;
@@ -408,6 +414,7 @@
}
@media (prefers-reduced-motion: no-preference) {
.kp-diagnose,
.kp-regler,
.kp-hinweis,
.kp-beispiel {
transition: opacity var(--dur-enter) var(--ease);
@@ -539,7 +546,8 @@
color: var(--fg-muted);
text-wrap: balance;
}
.kp-karte__diagnose {
.kp-karte__diagnose,
.kp-karte__regler {
width: 100%;
max-width: 320px;
margin-top: 1rem;