Kundenportal-Bühne: Terminplaner, Heatmap und Pinnwand als Live-Demos statt „Funktionen“
Die Station „Funktionen“ ist durch drei Stationen ersetzt (Gruppe „Funktionen zum Dazubuchen“). Auf dem MacBook läuft je eine erfundene Kunden-Website als Live-Komponente, auf dem iPhone die Portal-Ansicht der Funktion (echter Portal-Code, Fake-Daten): - Terminplaner (Salon Schnittstelle): Buchung im Widget, Bestätigung per Link, Eintrag im Kalender-Abo, Mitteilung „Neue Buchung …“ auf dem Handy - Website-Analyse (Boulderhalle Überhang): simulierte Klicks bauen die Heatmap auf (Algorithmus wie analytics/client/h.js); Maus über der Seite zeigt Zone + Klickzahl, darunter die Diagnose mit Rangliste und Satz (Beispieldaten) - News-Pinnwand (Ortsgemeinde Musterbach): „Aufhängen“ im Portal, Zettel fällt auf die Pinnwand Demos laden als eigene Chunks, laufen nur bei aktiver, sichtbarer Station und ohne reduzierte Bewegung. Unter 900 px zeigen die Karten die Website in der Handy-Fassung. Spezifikation: docs/superpowers/specs/2026-10-04-funktionen-demos.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9ad9ba2b7
commit
72275db9fa
39 files changed
+6567
-75
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { DemoZeiger } from "../gemeinsam";
|
||||
import { VEROEFFENTLICHEN_PUNKT, type PinnwandZustand } from "./ablauf";
|
||||
|
||||
// Zusätzlich hier verfügbar, wie in der Spezifikation der Demo vorgesehen.
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { VEROEFFENTLICHEN_PUNKT };
|
||||
|
||||
/** Wo der Finger wartet, bevor er zum Knopf gleitet (unten rechts). */
|
||||
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: PinnwandZustand;
|
||||
/** Mitte des Knopfes „Veröffentlichen“; Standard: VEROEFFENTLICHEN_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 „Veröffentlichen“ und tippt; danach fällt auf der Website der Zettel.
|
||||
*/
|
||||
const PinnwandHandy = ({ zustand, punkt = VEROEFFENTLICHEN_PUNKT }: Props) => {
|
||||
const ziel = zustand.phase === "warten" ? RUHE : punkt;
|
||||
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 PinnwandHandy;
|
||||
@@ -0,0 +1,380 @@
|
||||
import { useId } from "react";
|
||||
import type { DemoVariante } from "../gemeinsam";
|
||||
import { PINNWAND_START, type PinnwandZustand } from "./ablauf";
|
||||
import "./pinnwand.css";
|
||||
|
||||
/**
|
||||
* Die Beispiel-Website der Demo „News-Pinnwand“: erfundene Ortsgemeinde Musterbach (musterbach.example).
|
||||
* Desktop = 1512 x 893 (MacBook-Fläche), Handy = 402 x 874 (oben 59 / unten 34 frei für Statusleiste und Home-Balken).
|
||||
* Die Zettel sind der echten News-Pinnwand (embed/news.js) nachgebaut: Kork, Holzrahmen, Pastellzettel,
|
||||
* Polaroid mit Klebestreifen, Plakette „Neu“. Alle Bilder sind selbst gezeichnete SVG-Grafiken.
|
||||
* Die Website ist reine Kulisse: aria-hidden, keine fokussierbaren Elemente (div/span statt a/button).
|
||||
*/
|
||||
|
||||
// ── Grafiken ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Schlichtes erfundenes Wappen: Linde über einer Bachwelle in einem Schild. */
|
||||
const Wappen = ({ klasse }: { klasse?: string }) => {
|
||||
const id = useId().replace(/:/g, "");
|
||||
return (
|
||||
<svg className={klasse} viewBox="0 0 64 76" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<clipPath id={`${id}-schild`}>
|
||||
<path d="M32 3 59 11v26c0 17-13 29-27 35C18 66 5 54 5 37V11z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path d="M32 3 59 11v26c0 17-13 29-27 35C18 66 5 54 5 37V11z" fill="#f4ead3" />
|
||||
<g clipPath={`url(#${id}-schild)`}>
|
||||
<rect x="27.5" y="33" width="9" height="26" rx="2" fill="#7a5230" />
|
||||
<circle cx="32" cy="22" r="14" fill="#2f6b3f" />
|
||||
<circle cx="21" cy="30" r="10.5" fill="#3d8050" />
|
||||
<circle cx="43" cy="30" r="10.5" fill="#3d8050" />
|
||||
<circle cx="32" cy="30" r="9" fill="#2f6b3f" />
|
||||
<path d="M0 52c8 0 8-5 16-5s8 5 16 5 8-5 16-5 8 5 16 5v30H0z" fill="#4b86b4" />
|
||||
<path d="M0 61c8 0 8-5 16-5s8 5 16 5 8-5 16-5 8 5 16 5v20H0z" fill="#6aa0c8" />
|
||||
</g>
|
||||
<path d="M32 3 59 11v26c0 17-13 29-27 35C18 66 5 54 5 37V11z" fill="none" stroke="#fff" strokeWidth="3" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
/** Pony fürs Karussell (Körper, Hals, Kopf, Mähne, Beine), Farbe über currentColor. */
|
||||
const Pony = ({ x, y, farbe, spiegeln }: { x: number; y: number; farbe: string; spiegeln?: boolean }) => (
|
||||
<g transform={`translate(${x} ${y})${spiegeln ? " scale(-1 1)" : ""}`} color={farbe}>
|
||||
<ellipse cx="0" cy="0" rx="13" ry="7" fill="currentColor" />
|
||||
<path d="M7 -3 13 -14 20 -11 17 -3z" fill="currentColor" />
|
||||
<ellipse cx="19" cy="-11.5" rx="6.5" ry="3.6" transform="rotate(18 19 -11.5)" fill="currentColor" />
|
||||
<path d="M8 -4 12 -15" stroke="#fff6d6" strokeWidth="2.4" strokeLinecap="round" />
|
||||
<path d="M-12 -2c-6 1-7 7-6 11" fill="none" stroke="#fff6d6" strokeWidth="2.4" strokeLinecap="round" />
|
||||
<path d="M-7 5 -8 15M8 5 9 15" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" />
|
||||
</g>
|
||||
);
|
||||
|
||||
/** „Foto“ zum neuen Zettel: Kerwe-Karussell mit Lichterkette am Abend. */
|
||||
const FotoKerwe = () => {
|
||||
const id = useId().replace(/:/g, "");
|
||||
// Lichterkette: Punkte auf einer quadratischen Kurve (Durchhang)
|
||||
const kette = (y0: number, durchhang: number, anzahl: number) =>
|
||||
Array.from({ length: anzahl }, (_, i) => {
|
||||
const t = (i + 0.5) / anzahl;
|
||||
const x = -10 + 340 * t;
|
||||
const y = y0 + durchhang * 4 * t * (1 - t);
|
||||
return { x, y };
|
||||
});
|
||||
const oben = kette(20, 34, 12);
|
||||
const keile = Array.from({ length: 8 }, (_, i) => i);
|
||||
return (
|
||||
<svg viewBox="0 0 320 240" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<linearGradient id={`${id}-himmel`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stopColor="#14234a" />
|
||||
<stop offset="0.55" stopColor="#2e5288" />
|
||||
<stop offset="1" stopColor="#f0a45c" />
|
||||
</linearGradient>
|
||||
<radialGradient id={`${id}-glut`} cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0" stopColor="#ffd98a" stopOpacity="0.55" />
|
||||
<stop offset="1" stopColor="#ffd98a" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="320" height="240" fill={`url(#${id}-himmel)`} />
|
||||
{[[30, 18], [70, 44], [110, 12], [210, 40], [265, 16], [298, 52], [180, 22]].map(([x, y]) => (
|
||||
<circle key={`${x}-${y}`} cx={x} cy={y} r="1.3" fill="#fff" opacity="0.8" />
|
||||
))}
|
||||
<circle cx="160" cy="135" r="125" fill={`url(#${id}-glut)`} />
|
||||
{/* ferne Bäume */}
|
||||
<path d="M0 176c20-26 44-30 62-14 14-22 40-24 58-6 16-18 44-14 56 4 18-20 50-18 66 2 14-14 34-12 46 6 12 2 24 6 32 8v64H0z" fill="#1c3a40" />
|
||||
{/* Boden */}
|
||||
<rect y="190" width="320" height="50" fill="#1f3326" />
|
||||
<ellipse cx="160" cy="203" rx="104" ry="9" fill="#f0b865" opacity="0.28" />
|
||||
{/* Karussell: Sockel, Stangen, Ponys, Zeltdach */}
|
||||
<rect x="64" y="186" width="192" height="12" rx="3" fill="#8a5a34" />
|
||||
<rect x="64" y="186" width="192" height="3" fill="#c28a52" />
|
||||
{[88, 124, 160, 196, 232].map((x) => (
|
||||
<rect key={x} x={x - 1.6} y="112" width="3.2" height="76" fill="#f2d28a" />
|
||||
))}
|
||||
<Pony x={106} y={150} farbe="#f6ecd4" />
|
||||
<Pony x={142} y={160} farbe="#e07a3f" spiegeln />
|
||||
<Pony x={178} y={150} farbe="#f6ecd4" />
|
||||
<Pony x={214} y={160} farbe="#d9402f" spiegeln />
|
||||
<rect x="78" y="106" width="164" height="8" fill="#f2d28a" />
|
||||
{/* Dach mit Streifen */}
|
||||
{keile.map((i) => {
|
||||
const x0 = 70 + i * 22.5;
|
||||
const x1 = x0 + 22.5;
|
||||
return (
|
||||
<g key={i}>
|
||||
<path d={`M160 56 L${x0} 106 L${x1} 106 Z`} fill={i % 2 === 0 ? "#d9402f" : "#f6ecd4"} />
|
||||
<path d={`M${x0} 106 q11.25 12 22.5 0 z`} fill={i % 2 === 0 ? "#d9402f" : "#f6ecd4"} />
|
||||
<circle cx={x0 + 11.25} cy="113" r="2.6" fill="#ffd35a" />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
<path d="M160 56v-17" stroke="#f2d28a" strokeWidth="2.4" />
|
||||
<path d="M160 39l17 6-17 6z" fill="#ffd35a" />
|
||||
<circle cx="160" cy="56" r="4" fill="#ffd35a" />
|
||||
{/* Lichterkette */}
|
||||
<path d={`M-10 20Q160 88 330 20`} fill="none" stroke="#10182e" strokeWidth="1.4" />
|
||||
{oben.map(({ x, y }, i) => (
|
||||
<g key={i}>
|
||||
<circle cx={x} cy={y + 3} r="8" fill="#ffd35a" opacity="0.28" />
|
||||
<circle cx={x} cy={y + 3} r="3.6" fill={i % 3 === 0 ? "#ff9a6a" : "#ffd35a"} />
|
||||
</g>
|
||||
))}
|
||||
{/* Weinstand der Feuerwehr links */}
|
||||
<rect x="10" y="156" width="46" height="34" fill="#4a3426" />
|
||||
<rect x="15" y="164" width="36" height="18" fill="#ffd98a" opacity="0.85" />
|
||||
<path d="M6 156 33 138l27 18z" fill="#c03a2b" />
|
||||
<path d="M6 156h54" stroke="#f6ecd4" strokeWidth="3" strokeDasharray="6 6" />
|
||||
<circle cx="262" cy="170" r="14" fill="#16302a" />
|
||||
<rect x="260" y="176" width="4" height="16" fill="#16302a" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
/** Foto zum Zettel „Neue Bücherzelle“: kleine Zelle am Spielplatz, Bücher im Regal. */
|
||||
const FotoBuecherzelle = () => {
|
||||
const id = useId().replace(/:/g, "");
|
||||
const buchfarben = ["#d9402f", "#2f6b3f", "#e8b53a", "#3b78c4", "#f6ecd4", "#c9633a", "#5a8f7b"];
|
||||
const regal = (y: number, versatz: number) =>
|
||||
Array.from({ length: 6 }, (_, i) => {
|
||||
const hoehe = 12 + ((i * 5 + versatz) % 7);
|
||||
return (
|
||||
<rect
|
||||
key={`${y}-${i}`}
|
||||
x={138 + i * 7.6}
|
||||
y={y - hoehe}
|
||||
width="6.2"
|
||||
height={hoehe}
|
||||
fill={buchfarben[(i + versatz) % buchfarben.length]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<svg viewBox="0 0 320 240" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<linearGradient id={`${id}-himmel`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stopColor="#a9d6ee" />
|
||||
<stop offset="1" stopColor="#e9f6fb" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="320" height="240" fill={`url(#${id}-himmel)`} />
|
||||
<circle cx="268" cy="44" r="30" fill="#ffe9a8" opacity="0.5" />
|
||||
<circle cx="268" cy="44" r="19" fill="#ffd96a" />
|
||||
<ellipse cx="70" cy="48" rx="34" ry="9" fill="#fff" opacity="0.9" />
|
||||
<ellipse cx="92" cy="40" rx="22" ry="9" fill="#fff" opacity="0.9" />
|
||||
<ellipse cx="214" cy="30" rx="26" ry="7" fill="#fff" opacity="0.8" />
|
||||
{/* Hügel und Wiese */}
|
||||
<path d="M0 150c50-26 110-30 170-14s110 6 150-6v110H0z" fill="#9ccb7a" />
|
||||
<path d="M0 176c60-14 120-16 180-6s100 4 140-4v74H0z" fill="#7fb35f" />
|
||||
{/* Schaukel links */}
|
||||
<path d="M22 196 40 124M78 196 60 124M34 132h32" stroke="#5d7389" strokeWidth="4" strokeLinecap="round" fill="none" />
|
||||
<path d="M44 132v40M56 132v40" stroke="#5d7389" strokeWidth="1.6" />
|
||||
<rect x="40" y="170" width="20" height="4" rx="2" fill="#e8b53a" />
|
||||
{/* Weg */}
|
||||
<path d="M130 240c10-28 22-44 34-50h30c12 8 24 24 34 50z" fill="#ead9ae" />
|
||||
{/* Bücherzelle */}
|
||||
<ellipse cx="164" cy="194" rx="46" ry="6" fill="#3e6b3a" opacity="0.35" />
|
||||
<path d="M114 92 128 62h72l14 30z" fill="#7d2a1f" />
|
||||
<rect x="120" y="92" width="88" height="8" fill="#a8362a" />
|
||||
<rect x="128" y="100" width="72" height="92" fill="#b8392b" />
|
||||
<rect x="136" y="106" width="56" height="80" fill="#4a3022" />
|
||||
{regal(124, 0)}
|
||||
{regal(146, 3)}
|
||||
{regal(168, 5)}
|
||||
{regal(186, 2)}
|
||||
<path d="M136 124h56M136 146h56M136 168h56" stroke="#8a5a34" strokeWidth="2.4" />
|
||||
<rect x="136" y="106" width="56" height="80" fill="#cfe9f2" opacity="0.28" />
|
||||
<path d="M164 106v80M136 126h56M136 146h56M136 166h56" stroke="#b8392b" strokeWidth="2.6" />
|
||||
<rect x="136" y="106" width="56" height="80" fill="none" stroke="#b8392b" strokeWidth="3.4" />
|
||||
<rect x="140" y="78" width="48" height="10" rx="2" fill="#f6ecd4" />
|
||||
<path d="M146 83h36" stroke="#b8392b" strokeWidth="3" strokeLinecap="round" strokeDasharray="5 3" />
|
||||
<circle cx="190" cy="148" r="2.4" fill="#e8b53a" />
|
||||
{/* Blumen */}
|
||||
{[[96, 206, "#f6ecd4"], [108, 214, "#e8b53a"], [234, 208, "#f6ecd4"], [248, 216, "#d9402f"], [222, 220, "#e8b53a"]].map(
|
||||
([x, y, f]) => (
|
||||
<g key={`${x}-${y}`}>
|
||||
<path d={`M${x} ${Number(y) + 8}v-8`} stroke="#4f8a45" strokeWidth="1.6" />
|
||||
<circle cx={x as number} cy={y as number} r="3.4" fill={f as string} />
|
||||
</g>
|
||||
),
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Zettel ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type Papier = "sun" | "sky" | "coral" | "white";
|
||||
type PinneFarbe = "rot" | "blau" | "gelb" | "gruen";
|
||||
|
||||
type ZettelDaten = {
|
||||
id: string;
|
||||
art: "zettel" | "foto" | "neu";
|
||||
papier?: Papier;
|
||||
datum: string;
|
||||
titel: string;
|
||||
text: string;
|
||||
foto?: "buecherzelle" | "kerwe";
|
||||
pinne?: PinneFarbe;
|
||||
};
|
||||
|
||||
/** Der neue Zettel, den der Betrieb auf dem Handy veröffentlicht. */
|
||||
const NEUER_ZETTEL: ZettelDaten = {
|
||||
id: "kerwe",
|
||||
art: "neu",
|
||||
datum: "4. Okt",
|
||||
titel: "Kerwe 2026",
|
||||
text: "Freitag bis Montag auf dem Dorfplatz. Fahrgeschäfte, Musik und Weinstand der Feuerwehr.",
|
||||
foto: "kerwe",
|
||||
pinne: "rot",
|
||||
};
|
||||
|
||||
/** Zettel, die schon hängen. „sitzung“ liegt unter dem neuen Zettel und wird von ihm überdeckt. */
|
||||
const ALTE_ZETTEL: ZettelDaten[] = [
|
||||
{ id: "sitzung", art: "zettel", papier: "white", datum: "29. Sep", titel: "Sitzung des Gemeinderats", text: "Dienstag, 6. Okt., 19 Uhr im Bürgerhaus. Alle sind willkommen.", pinne: "blau" },
|
||||
{ id: "strasse", art: "zettel", papier: "sky", datum: "2. Okt", titel: "Hauptstraße ab 13.10. halbseitig gesperrt", text: "Kanalbau: Eine Ampel regelt den Verkehr.", pinne: "rot" },
|
||||
{ id: "buecherzelle", art: "foto", datum: "30. Sep", titel: "Neue Bücherzelle", text: "Am Spielplatz: bringen, tauschen, mitnehmen.", foto: "buecherzelle" },
|
||||
{ id: "sprechstunde", art: "zettel", papier: "sun", datum: "1. Okt", titel: "Sprechstunde", text: "Donnerstags 17–19 Uhr beim Ortsbürgermeister.", pinne: "gruen" },
|
||||
{ id: "gruenschnitt", art: "zettel", papier: "coral", datum: "25. Sep", titel: "Grünschnitt-Sammelstelle", text: "Samstags von 10 bis 13 Uhr am Bauhof.", pinne: "gelb" },
|
||||
];
|
||||
|
||||
type Platz = { x: number; y: number; b: number; r: number; wackel?: boolean };
|
||||
|
||||
/** Lage der Zettel auf der Korkfläche (Designpunkte, x/y = linke obere Ecke, b = Breite, r = Drehung in Grad). */
|
||||
const PLAETZE: Record<DemoVariante, Record<string, Platz>> = {
|
||||
desktop: {
|
||||
sitzung: { x: 326, y: 118, b: 290, r: -3 },
|
||||
strasse: { x: 26, y: 30, b: 292, r: -2, wackel: true },
|
||||
buecherzelle: { x: 34, y: 322, b: 292, r: -1.4 },
|
||||
sprechstunde: { x: 664, y: 14, b: 272, r: 1.8, wackel: true },
|
||||
gruenschnitt: { x: 652, y: 348, b: 276, r: -1.6 },
|
||||
kerwe: { x: 300, y: 76, b: 344, r: 2 },
|
||||
},
|
||||
handy: {
|
||||
sitzung: { x: 46, y: 70, b: 270, r: -3 },
|
||||
strasse: { x: 8, y: 372, b: 262, r: -2.2, wackel: true },
|
||||
buecherzelle: { x: 86, y: 568, b: 262, r: 1.6 },
|
||||
sprechstunde: { x: 20, y: 800, b: 270, r: 1.4 },
|
||||
gruenschnitt: { x: 70, y: 980, b: 262, r: -1.4 },
|
||||
kerwe: { x: 24, y: 22, b: 314, r: 1.8 },
|
||||
},
|
||||
};
|
||||
|
||||
const Pinne = ({ farbe }: { farbe: PinneFarbe }) => <span className={`pw-pinne pw-pinne--${farbe}`} />;
|
||||
|
||||
const Zettel = ({ daten, platz, variante }: { daten: ZettelDaten; platz: Platz; variante: DemoVariante }) => {
|
||||
const stil = { left: platz.x, top: platz.y, width: platz.b, ["--pw-r" as string]: `${platz.r}deg` };
|
||||
const neu = daten.art === "neu";
|
||||
const klassen = ["pw-zettel", neu ? "pw-zettel--neu" : "", platz.wackel ? "pw-wackel" : ""];
|
||||
if (daten.art === "zettel") {
|
||||
return (
|
||||
<div className={`${klassen.join(" ")} pw-papier pw-papier--${daten.papier}`} style={stil} data-zettel={daten.id}>
|
||||
<Pinne farbe={daten.pinne ?? "rot"} />
|
||||
<span className="pw-datum">{daten.datum}</span>
|
||||
<div className="pw-titel">{daten.titel}</div>
|
||||
<div className="pw-text">{daten.text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const Foto = daten.foto === "kerwe" ? FotoKerwe : FotoBuecherzelle;
|
||||
return (
|
||||
<div className={`${klassen.join(" ")} pw-polaroid`} style={stil} data-zettel={daten.id} data-variante={variante}>
|
||||
{neu ? <Pinne farbe={daten.pinne ?? "rot"} /> : <span className="pw-band" />}
|
||||
<div className="pw-foto">
|
||||
<Foto />
|
||||
</div>
|
||||
<div className="pw-unterschrift">
|
||||
<span className="pw-datum">{daten.datum}</span>
|
||||
<div className="pw-titel">{daten.titel}</div>
|
||||
<div className="pw-text">{daten.text}</div>
|
||||
</div>
|
||||
{neu ? <span className="pw-plakette">Neu</span> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Brett = ({ variante }: { variante: DemoVariante }) => (
|
||||
<div className="pw-brett">
|
||||
<div className="pw-kork">
|
||||
{ALTE_ZETTEL.map((z) => (
|
||||
<Zettel key={z.id} daten={z} platz={PLAETZE[variante][z.id]} variante={variante} />
|
||||
))}
|
||||
<Zettel daten={NEUER_ZETTEL} platz={PLAETZE[variante].kerwe} variante={variante} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Seite ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const NAVIGATION = ["Rathaus", "Leben in Musterbach", "Veranstaltungen", "Pinnwand"];
|
||||
|
||||
const Marke = () => (
|
||||
<div className="pw-marke">
|
||||
<Wappen klasse="pw-wappen" />
|
||||
<div className="pw-marke-text">
|
||||
<div className="pw-marke-klein">Ortsgemeinde</div>
|
||||
<div className="pw-marke-gross">Musterbach</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** `zustand` fehlt, solange der Taktgeber noch lädt: dann steht die Website im Anfangszustand da. */
|
||||
type Props = { zustand?: PinnwandZustand; variante: DemoVariante };
|
||||
|
||||
/** Die Website auf der MacBook-Fläche (desktop) oder in der Handy-Fassung (handy). */
|
||||
const PinnwandSeite = ({ zustand = PINNWAND_START, variante }: Props) => (
|
||||
<div className="pw-seite" lang="de" aria-hidden="true" data-variante={variante} data-phase={zustand.phase}>
|
||||
<div className="pw-kopf">
|
||||
<Marke />
|
||||
{variante === "desktop" ? (
|
||||
<div className="pw-nav">
|
||||
{NAVIGATION.map((n) => (
|
||||
<div key={n} className={`pw-nav-eintrag${n === "Pinnwand" ? " is-aktiv" : ""}`}>
|
||||
{n}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="pw-menue">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{variante === "desktop" ? (
|
||||
<div className="pw-links">
|
||||
<div className="pw-h1">Neues aus Musterbach</div>
|
||||
<div className="pw-lead">Aktuelles aus Rathaus, Vereinen und Nachbarschaft – frisch an die Pinnwand geheftet.</div>
|
||||
<div className="pw-kachel">
|
||||
<span className="pw-punkt" />
|
||||
<div>
|
||||
<div className="pw-kachel-kopf">Rathaus</div>
|
||||
<div className="pw-kachel-gross">Heute geöffnet</div>
|
||||
<div className="pw-kachel-klein">8 bis 12 Uhr</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pw-kachel">
|
||||
<div className="pw-kalender">
|
||||
<div className="pw-kalender-kopf">Okt.</div>
|
||||
<div className="pw-kalender-tag">7</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="pw-kachel-kopf">Müllkalender</div>
|
||||
<div className="pw-kachel-gross">Biotonne</div>
|
||||
<div className="pw-kachel-klein">Mittwoch, 7. Okt.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pw-titelzeile">
|
||||
<div className="pw-h1">Neues aus Musterbach</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Brett variante={variante} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PinnwandSeite;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useAblauf } from "../gemeinsam";
|
||||
|
||||
/**
|
||||
* Ablauf der Demo „News-Pinnwand“ (Ortsgemeinde Musterbach), Spezifikation 3.3.
|
||||
* Reine Logik, kein CSS: die Komponenten werden später per React.lazy nachgeladen.
|
||||
*
|
||||
* Geschichte: warten (kurz Ruhe) -> tippen (Tipp-Welle auf dem Handy auf „Veröffentlichen“) ->
|
||||
* faellt (der neue Zettel fällt auf die Pinnwand) -> haengt (Plakette „Neu“, bleibt stehen) -> von vorn.
|
||||
* Der neue Zettel blendet beim Übergang haengt -> warten aus (das regelt das CSS über data-phase).
|
||||
*/
|
||||
|
||||
export type PinnwandPhase = "warten" | "tippen" | "faellt" | "haengt";
|
||||
|
||||
export type PinnwandZustand = {
|
||||
phase: PinnwandPhase;
|
||||
/** Zähler der Tipps für DemoZeiger: 0 solange nicht getippt wird, sonst eine je Durchlauf wachsende Zahl. */
|
||||
klick: number;
|
||||
/** Zähler der Durchläufe (wächst auch, wenn der Ablauf pausiert und wieder von vorn beginnt). */
|
||||
runde: number;
|
||||
/** true = keine Bewegung erlaubt: die Demo zeigt ihren Endzustand (Zettel hängt). */
|
||||
statisch: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Platzhalter: Mitte des Knopfes „Veröffentlichen“ im Portal-Screenshot (Designpunkte der Handy-Fläche 402 x 874).
|
||||
* Der Einbau setzt später die echte Position aus der Aufnahme ein (Prop `punkt` von PinnwandHandy).
|
||||
*/
|
||||
export const VEROEFFENTLICHEN_PUNKT = { x: 201, y: 770 } as const;
|
||||
|
||||
/** Innere Schritte: der Finger gleitet erst zum Knopf (tippen), dann folgt der Tipp (ebenfalls Phase „tippen“). */
|
||||
const SCHRITTE: readonly { phase: PinnwandPhase; dauer: number; tipp?: boolean }[] = [
|
||||
{ phase: "warten", dauer: 1500 },
|
||||
{ phase: "tippen", dauer: 1000 },
|
||||
{ phase: "tippen", dauer: 450, tipp: true },
|
||||
{ phase: "faellt", dauer: 1200 },
|
||||
{ phase: "haengt", dauer: 5000 },
|
||||
];
|
||||
const DAUERN = SCHRITTE.map((s) => s.dauer);
|
||||
|
||||
/** Anfangszustand (Phase „warten“): die Bühne zeigt die Website damit sofort, bevor der Taktgeber geladen ist. */
|
||||
export const PINNWAND_START: PinnwandZustand = { phase: "warten", klick: 0, runde: 0, statisch: false };
|
||||
|
||||
/** Gesamtdauer eines Durchlaufs in Millisekunden (für Tests und die Feinabstimmung der Bühne). */
|
||||
export const PINNWAND_RUNDE_MS = DAUERN.reduce((a, b) => a + b, 0);
|
||||
|
||||
/**
|
||||
* Taktgeber der Demo. `laeuft` = Station aktiv, im Bild, Tab sichtbar, Bewegung erlaubt.
|
||||
* Läuft sie nicht, steht die Demo in Phase „warten“ (nichts bewegt sich, kein Timer);
|
||||
* mit `statisch` zeigt sie sofort den Endzustand „haengt“.
|
||||
*/
|
||||
export const usePinnwandAblauf = (laeuft: boolean, statisch: boolean): PinnwandZustand => {
|
||||
const { schritt, runde } = useAblauf(laeuft && !statisch, DAUERN, true);
|
||||
if (statisch) return { phase: "haengt", klick: 0, runde, statisch: true };
|
||||
const aktuell = SCHRITTE[Math.min(schritt, SCHRITTE.length - 1)];
|
||||
return {
|
||||
phase: aktuell.phase,
|
||||
klick: aktuell.tipp || schritt > 2 ? runde + 1 : 0,
|
||||
runde,
|
||||
statisch: false,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,614 @@
|
||||
/*
|
||||
* Demo „News-Pinnwand“: Website der erfundenen Ortsgemeinde Musterbach.
|
||||
* Alle Klassen mit Präfix pw-. Desktop 1512 x 893, Handy 402 x 874 (oben 59 / unten 34 frei).
|
||||
* Grundzustand statisch (Zettel hängt); Bewegung nur transform/opacity hinter prefers-reduced-motion: no-preference.
|
||||
*/
|
||||
|
||||
.pw-seite {
|
||||
--pw-gruen: #2f6b3f;
|
||||
--pw-gruen-dunkel: #234f2f;
|
||||
--pw-sand: #e9dcc4;
|
||||
--pw-sand-hell: #f7f2e6;
|
||||
--pw-tinte: #232a33;
|
||||
--pw-tinte-leise: #55605a;
|
||||
--pw-papier-sun: #fff6d6;
|
||||
--pw-papier-sky: #ddeefb;
|
||||
--pw-papier-coral: #ffe1da;
|
||||
--pw-papier-white: #fff;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: var(--pw-sand-hell);
|
||||
color: var(--pw-tinte);
|
||||
font-family: "Instrument Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
letter-spacing: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
}
|
||||
.pw-seite *,
|
||||
.pw-seite *::before,
|
||||
.pw-seite *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Kopf ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.pw-kopf {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 112px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 56px;
|
||||
background: var(--pw-gruen);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 0 rgb(0 0 0 / 0.1);
|
||||
z-index: 2;
|
||||
}
|
||||
.pw-marke {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.pw-wappen {
|
||||
display: block;
|
||||
width: 62px;
|
||||
height: 74px;
|
||||
filter: drop-shadow(0 2px 3px rgb(0 0 0 / 0.25));
|
||||
}
|
||||
.pw-marke-klein {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: rgb(255 255 255 / 0.82);
|
||||
}
|
||||
.pw-marke-gross {
|
||||
margin-top: 6px;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
.pw-nav {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pw-nav-eintrag {
|
||||
padding: 14px 24px;
|
||||
border-radius: 999px;
|
||||
color: rgb(255 255 255 / 0.9);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pw-nav-eintrag.is-aktiv {
|
||||
background: var(--pw-sand);
|
||||
color: var(--pw-gruen-dunkel);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ── Linke Spalte (Desktop) ────────────────────────────────────────────── */
|
||||
|
||||
.pw-links {
|
||||
position: absolute;
|
||||
left: 56px;
|
||||
top: 150px;
|
||||
width: 396px;
|
||||
}
|
||||
.pw-h1 {
|
||||
font-size: 66px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--pw-gruen-dunkel);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.pw-lead {
|
||||
margin-top: 24px;
|
||||
font-size: 24px;
|
||||
line-height: 1.45;
|
||||
color: var(--pw-tinte-leise);
|
||||
}
|
||||
.pw-kachel {
|
||||
margin-top: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 24px 28px;
|
||||
border-radius: 24px;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 0 0 2px #e6dbc3,
|
||||
0 14px 26px -16px rgb(50 45 25 / 0.4);
|
||||
}
|
||||
.pw-lead + .pw-kachel {
|
||||
margin-top: 40px;
|
||||
}
|
||||
.pw-kachel-kopf {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--pw-gruen);
|
||||
}
|
||||
.pw-kachel-gross {
|
||||
margin-top: 4px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.pw-kachel-klein {
|
||||
margin-top: 2px;
|
||||
font-size: 23px;
|
||||
color: var(--pw-tinte-leise);
|
||||
}
|
||||
.pw-punkt {
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #3fa35c;
|
||||
box-shadow: 0 0 0 8px rgb(63 163 92 / 0.2);
|
||||
margin: 0 8px;
|
||||
}
|
||||
.pw-kalender {
|
||||
flex: none;
|
||||
width: 84px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 2px var(--pw-gruen);
|
||||
text-align: center;
|
||||
}
|
||||
.pw-kalender-kopf {
|
||||
background: var(--pw-gruen);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 30px;
|
||||
}
|
||||
.pw-kalender-tag {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
line-height: 62px;
|
||||
color: var(--pw-gruen-dunkel);
|
||||
}
|
||||
|
||||
/* ── Pinnwand: Holzrahmen, Kork ────────────────────────────────────────── */
|
||||
|
||||
.pw-brett {
|
||||
position: absolute;
|
||||
left: 486px;
|
||||
top: 142px;
|
||||
width: 970px;
|
||||
height: 722px;
|
||||
padding: 18px;
|
||||
border-radius: 30px;
|
||||
background:
|
||||
repeating-linear-gradient(94deg, rgb(255 255 255 / 0.05) 0 3px, rgb(60 30 5 / 0.05) 3px 7px),
|
||||
linear-gradient(145deg, #bb8851, #9d6c3a 55%, #a8733f);
|
||||
box-shadow:
|
||||
0 0 0 1px rgb(90 55 25 / 0.55),
|
||||
inset 0 2px 0 rgb(255 235 200 / 0.35),
|
||||
0 34px 44px -26px rgb(60 35 10 / 0.55);
|
||||
}
|
||||
.pw-kork {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background-color: #c08a52;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%27260%27 height=%27260%27%3E%3Cfilter id=%27a%27%3E%3CfeTurbulence type=%27fractalNoise%27 baseFrequency=%271.1%27 numOctaves=%272%27 seed=%273%27/%3E%3CfeColorMatrix values=%270 0 0 0 .29 0 0 0 0 .16 0 0 0 0 .06 0 0 0 -2.4 1.25%27/%3E%3C/filter%3E%3Crect width=%27100%25%27 height=%27100%25%27 filter=%27url(%23a)%27/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%27220%27 height=%27220%27%3E%3Cfilter id=%27b%27%3E%3CfeTurbulence type=%27fractalNoise%27 baseFrequency=%27.85%27 numOctaves=%272%27 seed=%2711%27/%3E%3CfeColorMatrix values=%270 0 0 0 .95 0 0 0 0 .8 0 0 0 0 .58 0 0 0 2.6 -1.55%27/%3E%3C/filter%3E%3Crect width=%27100%25%27 height=%27100%25%27 filter=%27url(%23b)%27/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%27600%27 height=%27600%27%3E%3Cfilter id=%27c%27%3E%3CfeTurbulence type=%27fractalNoise%27 baseFrequency=%27.012%27 numOctaves=%273%27 seed=%277%27/%3E%3CfeColorMatrix values=%270 0 0 0 .45 0 0 0 0 .27 0 0 0 0 .12 0 0 0 1.4 -.45%27/%3E%3C/filter%3E%3Crect width=%27100%25%27 height=%27100%25%27 filter=%27url(%23c)%27/%3E%3C/svg%3E");
|
||||
box-shadow:
|
||||
inset 0 0 0 2px rgb(80 45 15 / 0.38),
|
||||
inset 0 8px 20px rgb(60 32 10 / 0.42);
|
||||
}
|
||||
|
||||
/* ── Zettel ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.pw-zettel {
|
||||
position: absolute;
|
||||
transform: rotate(var(--pw-r, 0deg));
|
||||
transform-origin: 50% 16px;
|
||||
color: var(--pw-tinte);
|
||||
}
|
||||
.pw-papier {
|
||||
padding: 40px 26px 28px;
|
||||
border-radius: 6px 6px 20px 6px;
|
||||
background: var(--pw-papier);
|
||||
box-shadow:
|
||||
0 12px 22px rgb(45 25 8 / 0.28),
|
||||
0 2px 3px rgb(45 25 8 / 0.2);
|
||||
}
|
||||
.pw-papier::after {
|
||||
/* weiche Papierkante unten rechts */
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(160deg, rgb(255 255 255 / 0.5), rgb(255 255 255 / 0) 38%, rgb(120 90 40 / 0.07));
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
.pw-papier--sun {
|
||||
--pw-papier: var(--pw-papier-sun);
|
||||
}
|
||||
.pw-papier--sky {
|
||||
--pw-papier: var(--pw-papier-sky);
|
||||
}
|
||||
.pw-papier--coral {
|
||||
--pw-papier: var(--pw-papier-coral);
|
||||
}
|
||||
.pw-papier--white {
|
||||
--pw-papier: var(--pw-papier-white);
|
||||
}
|
||||
.pw-polaroid {
|
||||
padding: 14px 14px 22px;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 14px 24px rgb(45 25 8 / 0.3),
|
||||
0 2px 3px rgb(45 25 8 / 0.2);
|
||||
}
|
||||
.pw-foto {
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: #dfe6ea;
|
||||
}
|
||||
.pw-foto svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.pw-polaroid:not(.pw-zettel--neu) .pw-foto {
|
||||
aspect-ratio: 1.8 / 1;
|
||||
}
|
||||
.pw-unterschrift {
|
||||
padding: 16px 8px 0;
|
||||
}
|
||||
.pw-datum {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: #5b6673;
|
||||
}
|
||||
.pw-titel {
|
||||
margin-top: 10px;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--pw-tinte);
|
||||
}
|
||||
.pw-text {
|
||||
margin-top: 10px;
|
||||
font-size: 22px;
|
||||
line-height: 1.4;
|
||||
color: var(--pw-tinte);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.pw-polaroid .pw-titel {
|
||||
font-size: 28px;
|
||||
}
|
||||
.pw-zettel--neu .pw-titel {
|
||||
font-size: 42px;
|
||||
line-height: 1.05;
|
||||
}
|
||||
/* wie in der Zettelfarbe „Sonne“ des Portals: gelbes Papier mit eingeklebtem Foto */
|
||||
.pw-polaroid.pw-zettel--neu {
|
||||
background: var(--pw-papier-sun);
|
||||
}
|
||||
.pw-zettel--neu .pw-foto {
|
||||
box-shadow: 0 0 0 6px #fff;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
/* Nadel (Reißzwecke) und Klebestreifen */
|
||||
.pw-pinne {
|
||||
position: absolute;
|
||||
left: calc(50% - 14px);
|
||||
top: 6px;
|
||||
z-index: 3;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
--pw-pf: #d9402f;
|
||||
background:
|
||||
radial-gradient(circle at 34% 30%, rgb(255 255 255 / 0.85) 0 9%, rgb(255 255 255 / 0) 30%),
|
||||
radial-gradient(circle at 50% 50%, var(--pw-pf) 0 55%, color-mix(in srgb, var(--pw-pf) 62%, #000) 100%);
|
||||
box-shadow:
|
||||
0 6px 6px -1px rgb(40 20 5 / 0.45),
|
||||
0 1px 2px rgb(40 20 5 / 0.5);
|
||||
}
|
||||
.pw-pinne--rot {
|
||||
--pw-pf: #d9402f;
|
||||
}
|
||||
.pw-pinne--blau {
|
||||
--pw-pf: #3b78c4;
|
||||
}
|
||||
.pw-pinne--gelb {
|
||||
--pw-pf: #f0b52e;
|
||||
}
|
||||
.pw-pinne--gruen {
|
||||
--pw-pf: #3a9a5b;
|
||||
}
|
||||
.pw-band {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -16px;
|
||||
z-index: 3;
|
||||
width: 104px;
|
||||
height: 32px;
|
||||
transform: translateX(-50%) rotate(-3deg);
|
||||
background: rgb(255 255 255 / 0.66);
|
||||
border-left: 3px dotted rgb(35 52 74 / 0.08);
|
||||
border-right: 3px dotted rgb(35 52 74 / 0.08);
|
||||
box-shadow: 0 2px 3px rgb(35 52 74 / 0.18);
|
||||
}
|
||||
|
||||
/* Plakette „Neu“ wie in der echten Pinnwand */
|
||||
.pw-plakette {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: -16px;
|
||||
z-index: 4;
|
||||
padding: 11px 22px;
|
||||
border-radius: 999px;
|
||||
background: #e4573d;
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.03em;
|
||||
transform: rotate(8deg);
|
||||
box-shadow: 0 5px 10px rgb(35 25 20 / 0.35);
|
||||
}
|
||||
|
||||
/* Der neue Zettel liegt über den anderen. Bevor er „veröffentlicht“ wird, ist er nicht da. */
|
||||
.pw-zettel--neu {
|
||||
z-index: 5;
|
||||
}
|
||||
.pw-seite[data-phase="warten"] .pw-zettel--neu,
|
||||
.pw-seite[data-phase="tippen"] .pw-zettel--neu {
|
||||
opacity: 0;
|
||||
}
|
||||
.pw-seite[data-phase="warten"] .pw-plakette,
|
||||
.pw-seite[data-phase="tippen"] .pw-plakette,
|
||||
.pw-seite[data-phase="faellt"] .pw-plakette {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ── Handy-Fassung (402 x 874, oben 59 / unten 34 frei) ───────────────── */
|
||||
|
||||
.pw-seite[data-variante="handy"] .pw-kopf {
|
||||
height: 127px;
|
||||
padding: 59px 20px 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-wappen {
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-marke {
|
||||
gap: 12px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-marke-klein {
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-marke-gross {
|
||||
margin-top: 4px;
|
||||
font-size: 27px;
|
||||
}
|
||||
.pw-menue {
|
||||
margin-left: auto;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
width: 30px;
|
||||
}
|
||||
.pw-menue i {
|
||||
display: block;
|
||||
height: 3.5px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
}
|
||||
.pw-titelzeile {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
top: 142px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-h1 {
|
||||
font-size: 30px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-brett {
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
top: 192px;
|
||||
width: auto;
|
||||
height: 760px;
|
||||
padding: 10px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-kork {
|
||||
border-radius: 13px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-papier {
|
||||
padding: 32px 20px 22px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-polaroid {
|
||||
padding: 10px 10px 16px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-foto {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-unterschrift {
|
||||
padding: 12px 6px 0;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-datum {
|
||||
font-size: 15px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-titel,
|
||||
.pw-seite[data-variante="handy"] .pw-polaroid .pw-titel {
|
||||
margin-top: 8px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-zettel--neu .pw-titel {
|
||||
margin-top: 8px;
|
||||
font-size: 26px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-text {
|
||||
margin-top: 6px;
|
||||
font-size: 15.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-pinne {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
left: calc(50% - 11px);
|
||||
top: 5px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-band {
|
||||
width: 84px;
|
||||
height: 26px;
|
||||
top: -13px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] .pw-plakette {
|
||||
top: -16px;
|
||||
right: -10px;
|
||||
padding: 9px 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* ── Bewegung: nur transform und opacity, nur ohne reduzierte Bewegung ─── */
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.pw-seite {
|
||||
--pw-fall: 660px;
|
||||
}
|
||||
.pw-seite[data-variante="handy"] {
|
||||
--pw-fall: 600px;
|
||||
}
|
||||
/* wieder weg: der neue Zettel blendet vor dem nächsten Durchlauf aus */
|
||||
.pw-zettel--neu {
|
||||
transition: opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.pw-seite[data-phase="faellt"] .pw-zettel--neu {
|
||||
animation: pw-fall 1200ms both;
|
||||
}
|
||||
.pw-seite[data-phase="faellt"] .pw-zettel--neu .pw-pinne {
|
||||
animation: pw-sticht 1200ms both;
|
||||
}
|
||||
.pw-seite[data-phase="haengt"] .pw-plakette {
|
||||
animation: pw-plakette 520ms cubic-bezier(0.2, 0.8, 0.2, 1) 120ms both;
|
||||
}
|
||||
/* die Nachbarn wackeln beim Aufprall leicht mit */
|
||||
.pw-seite[data-phase="faellt"] .pw-wackel {
|
||||
animation: pw-wackeln 800ms ease-in-out 480ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pw-fall {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(calc(var(--pw-fall) * -1)) rotate(calc(var(--pw-r) - 8deg));
|
||||
animation-timing-function: cubic-bezier(0.55, 0.05, 0.85, 0.5);
|
||||
}
|
||||
6% {
|
||||
opacity: 1;
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: translateY(8px) rotate(calc(var(--pw-r) + 7deg));
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
54% {
|
||||
transform: translateY(0) rotate(calc(var(--pw-r) - 4.5deg));
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
68% {
|
||||
transform: translateY(0) rotate(calc(var(--pw-r) + 2.6deg));
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
82% {
|
||||
transform: translateY(0) rotate(calc(var(--pw-r) - 1deg));
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) rotate(var(--pw-r));
|
||||
}
|
||||
}
|
||||
@keyframes pw-sticht {
|
||||
0%,
|
||||
38% {
|
||||
opacity: 0;
|
||||
transform: scale(1.9);
|
||||
}
|
||||
44% {
|
||||
opacity: 1;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
56% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
70%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes pw-plakette {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(8deg) scale(0.4);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: rotate(8deg) scale(1.15);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(8deg) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes pw-wackeln {
|
||||
0% {
|
||||
transform: rotate(var(--pw-r));
|
||||
}
|
||||
25% {
|
||||
transform: rotate(calc(var(--pw-r) + 1.1deg));
|
||||
}
|
||||
55% {
|
||||
transform: rotate(calc(var(--pw-r) - 0.7deg));
|
||||
}
|
||||
80% {
|
||||
transform: rotate(calc(var(--pw-r) + 0.3deg));
|
||||
}
|
||||
100% {
|
||||
transform: rotate(var(--pw-r));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user