Kundenportal-Bühne: Gruppe „Weitere Funktionen“, Unterpunkte als Stichpunkte

Wunsch des Inhabers: Die drei Funktionen heißen als Gruppe „Weitere Funktionen“ und
beschreiben sich in drei Stichpunkten statt in einem Satz (Liste der Bühne und Karten
der Wisch-Reihe). Der Bedienhinweis der Heatmap („Zeigen Sie mit der Maus …“ bzw.
„Tippen Sie …“) steht jetzt im Hinweis unter dem MacBook und in der Karte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-04 15:02:04 +00:00
1 parent 72275db9fa
commit f0bced164b
4 files changed
+131 -36

No files matched your search

+53 -14
View File
@@ -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 <button> (nur Phrasing-Inhalt
* erlaubt), darum sind die Stichpunkte dort Blöcke aus <span>; das versteckte Komma trennt sie für Screenreader.
*/
const Unterschrift = ({ f }: { f: Funktion }) =>
f.stichpunkte ? (
<>
{f.stichpunkte.map((p, i) => (
<span key={p} className="kp-stichpunkt">
{p}
{i < f.stichpunkte!.length - 1 ? <span className="sr-only">, </span> : null}
</span>
))}
</>
) : (
<>{f.unterschrift}</>
);
/** Unterschrift in den Karten der Wisch-Reihe (außerhalb eines Knopfs): Satz oder echte Liste. */
const KartenUnterschrift = ({ f }: { f: Funktion }) =>
f.stichpunkte ? (
<ul className="wk-muted kp-karte__punkte">
{f.stichpunkte.map((p) => (
<li key={p}>{p}</li>
))}
</ul>
) : (
<p className="wk-muted kp-karte__text">{f.unterschrift}</p>
);
const SCHRITTE = [
{ nr: 1, text: "Kennenlernen" },
{ nr: 2, text: "Angebot mit Preis" },
@@ -400,8 +433,9 @@ const Buehne = () => {
>
<span className="kp-eintrag__punkt" aria-hidden="true" />
<span className="kp-eintrag__titel">{f.titel}</span>
{/* ohne Maus (Tablet) heißt es „Tippen Sie“ statt „Zeigen Sie mit der Maus“ */}
<span className="kp-eintrag__text">{!fein && f.unterschriftHandy ? f.unterschriftHandy : f.unterschrift}</span>
<span className="kp-eintrag__text">
<Unterschrift f={f} />
</span>
</button>
);
if (!f.demo) return <li key={f.id}>{eintrag}</li>;
@@ -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)}
</p>
))}
</div>
@@ -543,7 +581,7 @@ const LangeKarte = ({ f }: { f: Funktion }) => {
</div>
</HandyRahmen>
<h3 className="wk-h3 kp-karte__titel">{f.titel}</h3>
<p className="wk-muted kp-karte__text">{f.unterschrift}</p>
<KartenUnterschrift f={f} />
</div>
);
};
@@ -569,8 +607,9 @@ const DemoKarteRahmen = ({
</HandyRahmen>
</div>
<h3 className="wk-h3 kp-karte__titel">{f.titel}</h3>
<p className="wk-muted kp-karte__text">{f.unterschriftHandy ?? f.unterschrift}</p>
<KartenUnterschrift f={f} />
<p className="kp-karte__beispiel">{beispielText(f)}</p>
{f.bedienhinweis ? <p className="kp-karte__bedienen">{f.bedienhinweis.tippen}</p> : null}
{nachher}
</div>
);
@@ -650,7 +689,7 @@ const HandyReihe = () => (
<div key={f.id} className="kp-karte">
<HandyRahmen className="kp-karte__handy" bild={handyBild(f.handy.name, f.handy.alt, "250px")} />
<h3 className="wk-h3 kp-karte__titel">{f.titel}</h3>
<p className="wk-muted kp-karte__text">{f.unterschrift}</p>
<KartenUnterschrift f={f} />
</div>
);
})}
+48 -1
View File
@@ -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 {
+22 -16
View File
@@ -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"]);