diff --git a/docs/superpowers/specs/2026-10-04-funktionen-demos.md b/docs/superpowers/specs/2026-10-04-funktionen-demos.md index 101db5c..f1a849c 100644 --- a/docs/superpowers/specs/2026-10-04-funktionen-demos.md +++ b/docs/superpowers/specs/2026-10-04-funktionen-demos.md @@ -14,16 +14,19 @@ Dieses Dokument ergänzt `2026-10-01-relaunch-design.md` (Abschnitt 7.2 Kundenpo ## 1. Stationen der Bühne (Reihenfolge) 1. Live-Vorschau · 2. Fortschritt · 3. Chat · 4. Rechnungen (unverändert) -5. **Terminplaner** · 6. **Website-Analyse** · 7. **News-Pinnwand** – Gruppe „Funktionen zum Dazubuchen“ +5. **Terminplaner** · 6. **Website-Analyse** · 7. **News-Pinnwand** – Gruppe „Weitere Funktionen“ (Inhaber 2026-10-04: so nennen, Unterpunkte nur in Stichpunkten) 8. Als App (unverändert, bleibt die letzte Station mit dem langen Screenshot) Texte (Sie-Form, keine Erfolgsversprechen, keine Zahlen ohne Quelle): -| Station | Titel | Unterschrift | +| Station | Titel | Stichpunkte | |---|---|---| -| 5 | Terminplaner | Gäste buchen auf Ihrer Website – die Termine sehen Sie im Portal und auf Wunsch im Google Kalender. | -| 6 | Website-Analyse | Sehen Sie, wohin Ihre Besucher klicken. Zeigen Sie mit der Maus auf die Seite. | -| 7 | News-Pinnwand | Neuigkeiten mit Text und Foto, vom Handy direkt auf Ihre Website. | +| 5 | Terminplaner | Gäste buchen direkt auf Ihrer Website · Termine und Anfragen im Portal · Kalender-Abo, z. B. Google Kalender | +| 6 | Website-Analyse | Heatmap: wohin Besucher klicken · Aufrufe, Herkunft und Geräte · Im Wartungs-Abo enthalten | +| 7 | News-Pinnwand | Text und Foto, vom Handy aus · Hängt direkt auf Ihrer Website · Laufzeit je Zettel wählbar | + +Bedienhinweis der Heatmap (nicht in den Stichpunkten, sondern im Hinweis unter dem MacBook bzw. in der Karte): +„Zeigen Sie mit der Maus auf die Seite.“ (Maus) / „Tippen Sie auf eine Stelle der Seite.“ (Touch). Wahrheit der Aussagen (geprüft im Portal-Code und in den Spezifikationen): - Terminplaner: Buchung im Widget, Gast bestätigt per Link in der E-Mail, Betrieb bekommt eine Mail diff --git a/src/components/home/Kundenportal.tsx b/src/components/home/Kundenportal.tsx index 4e2a984..db94972 100644 --- a/src/components/home/Kundenportal.tsx +++ b/src/components/home/Kundenportal.tsx @@ -55,9 +55,12 @@ type DemoArt = "termin" | "heatmap" | "pinnwand"; type Funktion = { id: string; titel: string; - unterschrift: string; - /** Abweichende Unterschrift in der Wisch-Reihe (dort gibt es keine Maus). */ - unterschriftHandy?: string; + /** Ein Satz unter dem Titel (Portal-Stationen); die Funktionen haben stattdessen Stichpunkte. */ + unterschrift?: string; + /** Stichpunkte unter dem Titel (die drei Funktionen, Wunsch des Inhabers 2026-10-04). */ + stichpunkte?: string[]; + /** Bedienhinweis zur Live-Demo, je nachdem, ob es eine Maus gibt (nur Website-Analyse). */ + bedienhinweis?: { maus: string; tippen: string }; /** Desktop-Bild; Demo-Stationen zeigen stattdessen die Website, "Als App" hat keines (dort blendet das Fenster aus). */ desktop?: { name: string; alt: string }; handy: { name: string; alt: string }; @@ -116,7 +119,7 @@ const FUNKTIONEN: Funktion[] = [ { id: "terminplaner", titel: "Terminplaner", - unterschrift: "Gäste buchen auf Ihrer Website – die Termine sehen Sie im Portal und auf Wunsch im Google Kalender.", + stichpunkte: ["Gäste buchen direkt auf Ihrer Website", "Termine und Anfragen im Portal", "Kalender-Abo, z. B. Google Kalender"], demo: "termin", adresse: "salon-schnittstelle.example", beschreibung: @@ -131,8 +134,8 @@ const FUNKTIONEN: Funktion[] = [ { id: "website-analyse", titel: "Website-Analyse", - unterschrift: "Sehen Sie, wohin Ihre Besucher klicken. Zeigen Sie mit der Maus auf die Seite.", - unterschriftHandy: "Sehen Sie, wohin Ihre Besucher klicken. Tippen Sie auf eine Stelle der Seite.", + stichpunkte: ["Heatmap: wohin Besucher klicken", "Aufrufe, Herkunft und Geräte", "Im Wartungs-Abo enthalten"], + bedienhinweis: { maus: "Zeigen Sie mit der Maus auf die Seite.", tippen: "Tippen Sie auf eine Stelle der Seite." }, demo: "heatmap", adresse: "boulderhalle-ueberhang.example", beschreibung: @@ -145,7 +148,7 @@ const FUNKTIONEN: Funktion[] = [ { id: "news-pinnwand", titel: "News-Pinnwand", - unterschrift: "Neuigkeiten mit Text und Foto, vom Handy direkt auf Ihre Website.", + stichpunkte: ["Text und Foto, vom Handy aus", "Hängt direkt auf Ihrer Website", "Laufzeit je Zettel wählbar"], demo: "pinnwand", adresse: "musterbach.example", beschreibung: "Beispiel-Website einer Ortsgemeinde mit einer Pinnwand, an der ein neuer Zettel aufgehängt wird", @@ -171,7 +174,7 @@ 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; -const GRUPPE_TITEL = "Funktionen zum Dazubuchen"; +const GRUPPE_TITEL = "Weitere Funktionen"; // ── Demos (eigener Chunk, erst geladen, wenn die Bühne die Station fast erreicht) ────────────────────── @@ -214,6 +217,36 @@ const INERT = (parseInt(version, 10) >= 19 ? { inert: true } : { inert: "" }) as const beispielText = (f: Funktion) => `Erfundenes Beispiel · ${f.adresse}`; +/** + * Unterschrift einer Station: ein Satz oder Stichpunkte. In der Liste steckt sie in einem ); if (!f.demo) return
  • {eintrag}
  • ; @@ -484,7 +518,11 @@ const Buehne = () => { data-an={f.demo === aktivDemo ? "ja" : "nein"} aria-hidden={f.demo !== aktivDemo || undefined} > - {beispielText(f)} + {/* Website-Analyse: statt der Adresse (steht in der Safari-Leiste) der Bedienhinweis, damit es eine Zeile bleibt; + ohne Maus (Tablet quer) heißt es „Tippen Sie“ statt „Zeigen Sie mit der Maus“ */} + {f.bedienhinweis + ? `Erfundenes Beispiel · ${fein ? f.bedienhinweis.maus : f.bedienhinweis.tippen}` + : beispielText(f)}

    ))} @@ -543,7 +581,7 @@ const LangeKarte = ({ f }: { f: Funktion }) => {

    {f.titel}

    -

    {f.unterschrift}

    + ); }; @@ -569,8 +607,9 @@ const DemoKarteRahmen = ({

    {f.titel}

    -

    {f.unterschriftHandy ?? f.unterschrift}

    +

    {beispielText(f)}

    + {f.bedienhinweis ?

    {f.bedienhinweis.tippen}

    : null} {nachher} ); @@ -650,7 +689,7 @@ const HandyReihe = () => (

    {f.titel}

    -

    {f.unterschrift}

    +
    ); })} diff --git a/src/components/home/kundenportal.css b/src/components/home/kundenportal.css index edc7c39..5ea81b8 100644 --- a/src/components/home/kundenportal.css +++ b/src/components/home/kundenportal.css @@ -114,6 +114,22 @@ .kp-eintrag[aria-current="step"] .kp-eintrag__titel { color: var(--fg); } +/* Stichpunkte der drei Funktionen: je Punkt eine Zeile mit kleinem runden Punkt davor */ +.kp-stichpunkt { + position: relative; + display: block; + padding-left: 0.95em; +} +.kp-stichpunkt::before { + content: ""; + position: absolute; + left: 0.15em; + top: calc(0.7em - 2px); + width: 4px; + height: 4px; + border-radius: 50%; + background: currentColor; +} /* kleiner roter Punkt als Marker der aktiven Funktion (hängt im Rand links vom Text) */ .kp-eintrag__punkt { position: absolute; @@ -178,7 +194,7 @@ } } -/* Gruppe "Funktionen zum Dazubuchen": die drei Demo-Stationen mit kleiner Überschrift darüber */ +/* Gruppe "Weitere Funktionen": die drei Demo-Stationen mit kleiner Überschrift darüber */ .kp-gruppe[data-erste="ja"] { margin-top: 10px; } @@ -375,6 +391,37 @@ .kp-karte__text { max-width: 26ch; } +/* Stichpunkte in den Karten: linksbündige Liste, als Block in der Kartenmitte */ +.kp-karte__punkte { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + margin: 0; + padding: 0; + list-style: none; + text-align: left; +} +.kp-karte__punkte li { + position: relative; + padding-left: 0.95em; +} +.kp-karte__punkte li::before { + content: ""; + position: absolute; + left: 0.15em; + top: calc(0.7em - 2px); + width: 4px; + height: 4px; + border-radius: 50%; + background: currentColor; +} +.kp-karte__bedienen { + margin: 0.35rem 0 0; + font-size: var(--fs-small); + font-weight: 600; + color: var(--fg); +} /* Demo-Karten: die Website läuft im Handy, darunter Hinweis aufs erfundene Beispiel (Heatmap: zusätzlich die Diagnose) */ .kp-karte__geraet { diff --git a/src/test/relaunch-kundenportal.test.tsx b/src/test/relaunch-kundenportal.test.tsx index f277966..88a9568 100644 --- a/src/test/relaunch-kundenportal.test.tsx +++ b/src/test/relaunch-kundenportal.test.tsx @@ -11,14 +11,15 @@ const WURZEL = `${process.cwd()}/`; const BILDORDNER = join(WURZEL, "public/relaunch/portal"); const TITEL = ["Live-Vorschau", "Fortschritt", "Chat", "Rechnungen", "Terminplaner", "Website-Analyse", "News-Pinnwand", "Als App"]; -const UNTERSCHRIFTEN = [ +/** Ein Satz je Portal-Station, Stichpunkte bei den drei Funktionen (Wunsch des Inhabers 2026-10-04). */ +const UNTERSCHRIFTEN: (string | string[])[] = [ "Ihre Website, noch während wir daran bauen.", "Was wir zuletzt erledigt haben – in einfachen Sätzen.", "Frage, Foto oder Sprachnachricht direkt ans Team.", "Ansehen, bezahlen, herunterladen.", - "Gäste buchen auf Ihrer Website – die Termine sehen Sie im Portal und auf Wunsch im Google Kalender.", - "Sehen Sie, wohin Ihre Besucher klicken. Zeigen Sie mit der Maus auf die Seite.", - "Neuigkeiten mit Text und Foto, vom Handy direkt auf Ihre Website.", + ["Gäste buchen direkt auf Ihrer Website", "Termine und Anfragen im Portal", "Kalender-Abo, z. B. Google Kalender"], + ["Heatmap: wohin Besucher klicken", "Aufrufe, Herkunft und Geräte", "Im Wartungs-Abo enthalten"], + ["Text und Foto, vom Handy aus", "Hängt direkt auf Ihrer Website", "Laufzeit je Zettel wählbar"], "Aufs Handy installieren, fertig.", ]; const SCHRITTE = [ @@ -90,9 +91,14 @@ describe("Kundenportal: Text und Reihenfolge", () => { it("Handy: die acht Funktionen stehen in der richtigen Reihenfolge mit ihren Unterschriften", () => { zeigeHandy(); expect(screen.getAllByRole("heading", { level: 3 }).map((h) => h.textContent)).toEqual(TITEL); - // in der Wisch-Reihe gibt es keine Maus: die Analyse sagt "Tippen" statt "mit der Maus zeigen" - const handyTexte = UNTERSCHRIFTEN.map((t) => (t.includes("mit der Maus") ? "Sehen Sie, wohin Ihre Besucher klicken. Tippen Sie auf eine Stelle der Seite." : t)); - for (const text of handyTexte) expect(screen.getByText(text)).toBeInTheDocument(); + for (const text of UNTERSCHRIFTEN.flat()) expect(screen.getByText(text)).toBeInTheDocument(); + // Stichpunkte stehen in den Karten als echte Liste + for (const punkte of UNTERSCHRIFTEN.filter(Array.isArray)) { + expect(screen.getByText(punkte[0]).closest("ul")).toHaveClass("kp-karte__punkte"); + } + // in der Wisch-Reihe gibt es keine Maus: der Bedienhinweis der Analyse sagt "Tippen" + expect(screen.getByText("Tippen Sie auf eine Stelle der Seite.")).toBeInTheDocument(); + expect(screen.queryByText(/mit der Maus/)).toBeNull(); }); it("Desktop: die Liste der Bühne hat dieselben acht Funktionen in derselben Reihenfolge, die drei Demos als Gruppe", () => { @@ -102,22 +108,21 @@ describe("Kundenportal: Text und Reihenfolge", () => { expect(eintraege).toHaveLength(8); TITEL.forEach((titel, i) => { expect(eintraege[i]).toHaveTextContent(new RegExp(`^${titel}`)); - expect(eintraege[i]).toHaveTextContent(UNTERSCHRIFTEN[i]); + for (const text of [UNTERSCHRIFTEN[i]].flat()) expect(eintraege[i]).toHaveTextContent(text); }); // Gruppenüberschrift steht über dem ersten der drei Funktions-Einträge, die Gruppe umfasst genau diese drei - const gruppe = screen.getByText("Funktionen zum Dazubuchen"); + const gruppe = screen.getByText("Weitere Funktionen"); expect(gruppe.closest("li")).toContainElement(eintraege[4]); expect(Array.from(liste.querySelectorAll("li.kp-gruppe"))).toHaveLength(3); expect(eintraege[3].closest("li")).not.toHaveClass("kp-gruppe"); expect(eintraege[7].closest("li")).not.toHaveClass("kp-gruppe"); }); - it("Tablet quer (Bühne ohne Maus): die Unterschrift der Website-Analyse sagt „Tippen Sie“ statt „Zeigen Sie mit der Maus“", () => { - zeigeTablet(); - const liste = screen.getByRole("list", { name: "Funktionen des Kundenportals" }); - const analyse = within(liste).getAllByRole("button")[5]; - expect(analyse).toHaveTextContent("Tippen Sie auf eine Stelle der Seite."); - expect(analyse).not.toHaveTextContent("Maus"); + it("Tablet quer (Bühne ohne Maus): der Bedienhinweis der Website-Analyse sagt „Tippen Sie“ statt „Zeigen Sie mit der Maus“", () => { + const { container } = zeigeTablet(); + const hinweis = container.querySelectorAll(".kp-beispiel")[1]; + expect(hinweis).toHaveTextContent("Tippen Sie auf eine Stelle der Seite."); + expect(hinweis).not.toHaveTextContent("Maus"); }); it.each([ @@ -396,7 +401,8 @@ describe("Kundenportal: Bühne wechselt mit dem IntersectionObserver", () => { const hinweise = Array.from(container.querySelectorAll(".kp-beispiel")); expect(hinweise.map((h) => h.textContent)).toEqual([ "Erfundenes Beispiel · salon-schnittstelle.example", - "Erfundenes Beispiel · boulderhalle-ueberhang.example", + // mit Maus (diese Bühne meldet eine feine Zeigereingabe): Hinweis zum Zeigen + "Erfundenes Beispiel · Zeigen Sie mit der Maus auf die Seite.", "Erfundenes Beispiel · musterbach.example", ]); expect(hinweise.map((h) => h.getAttribute("data-an"))).toEqual(["nein", "ja", "nein"]);