From e0e81b65a85fe74187cefa6494d879c471bddeb6 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 1 Oct 2026 18:30:10 +0000 Subject: [PATCH] Kundenportal: realistische MacBook- und iPhone-Rahmen aus KNSONWS/homepage (device-mockups) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeviceFrames.tsx und device-frames.css unverändert übernommen (Commit ae123f0). DesktopFenster/HandyRahmen nutzen sie jetzt; Anordnung wie in der Vorlage (iPhone über der rechten unteren Ecke), Screenshots füllen die Bildschirmfläche. Co-Authored-By: Claude Opus 5.5 --- src/components/GeraeteRahmen.tsx | 30 +- src/components/device-frames/DeviceFrames.tsx | 372 +++++++++ .../device-frames/device-frames.css | 786 ++++++++++++++++++ src/components/geraete-rahmen.css | 85 +- src/components/home/kundenportal.css | 28 +- src/test/relaunch-kundenportal.test.tsx | 14 +- 6 files changed, 1203 insertions(+), 112 deletions(-) create mode 100644 src/components/device-frames/DeviceFrames.tsx create mode 100644 src/components/device-frames/device-frames.css diff --git a/src/components/GeraeteRahmen.tsx b/src/components/GeraeteRahmen.tsx index 19bcdb3..7a3d1fb 100644 --- a/src/components/GeraeteRahmen.tsx +++ b/src/components/GeraeteRahmen.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { MacBookFrame, IPhoneFrame } from "./device-frames/DeviceFrames"; import "./geraete-rahmen.css"; /** Ein Bild samt Größenangaben. Alle Felder landen 1:1 im (Spezifikation 9.5). */ @@ -47,31 +48,24 @@ type FensterProps = RahmenProps & { }; /** - * Desktop-Fenster (Spezifikation 7.2): 1 px Linie, 13 px Rundung, oben eine ruhige Leiste mit - * der Adresse, darunter das Bild im Seitenverhältnis 16:10. Die Farben kommen aus den Tokens, - * so passt der Rahmen auf hellen und dunklen Grund. + * Desktop: MacBook Pro 14" mit Safari und der Adresse project.webklar.com (Rahmen aus + * components/device-frames, übernommen aus KNSONWS/homepage „device-mockups“). Der Inhalt wird + * in 1512 x 893 gesetzt und verkleinert; der Screenshot füllt die Fläche von oben. */ export const DesktopFenster = ({ bild, children, className, adresse = "project.webklar.com" }: FensterProps) => ( -
- {/* reine Dekoration: der Inhalt steht im Alt-Text des Bildes */} - -
{bild ? : children}
-
+ +
{bild ? : children}
+
); /** - * Handy-Rahmen: abgerundete Gerätekontur (etwa 44 px Rundung bei 390 px Breite, alles in cqw, - * damit der Rahmen mit jeder Größe mitwächst), dünner Rand, Bild im Seitenverhältnis 780 x 1688. - * Die Breite bestimmt der Aufrufer (width oder Spalte); der Rahmen füllt sie. + * Handy: iPhone 16 Pro (Inhalt in 402 x 874). Die Portal-Aufnahmen haben oben nur 47 px (Roh, 2x) + * Rand für die Statusleiste; damit Dynamic Island nichts verdeckt, beginnt das Bild 35 px tiefer. */ export const HandyRahmen = ({ bild, children, className }: RahmenProps) => ( -
-
-
{bild ? : children}
-
-
+ +
{bild ? : children}
+
); type GeraeteRahmenProps = (FensterProps & { art: "desktop" }) | (RahmenProps & { art: "handy" }); diff --git a/src/components/device-frames/DeviceFrames.tsx b/src/components/device-frames/DeviceFrames.tsx new file mode 100644 index 0000000..955adbc --- /dev/null +++ b/src/components/device-frames/DeviceFrames.tsx @@ -0,0 +1,372 @@ +/* + * DeviceFrames.tsx – React-Komponenten für die Geräterahmen + * (MacBook Pro 14" und iPhone 16 Pro, Styles in device-frames.css) + * + * import { MacBookFrame, IPhoneFrame } from './DeviceFrames'; + * + * + * // gesetzt in 1512 x 893 (Safari-Viewport) + * + * + * // gesetzt in 402 x 874, oben 59 / unten 34 frei lassen + * + * + * Die Breite des Wurzelelements bestimmt die Größe, alles andere skaliert mit + * (beim MacBook ist das die Breite des Unterteils, der Deckel ist 87,7 % davon). + * device-frames.js wird in React nicht gebraucht: der Hook unten skaliert. + * Weitere Attribute (id, aria-hidden, data-*, Events …) gehen an das Wurzelelement. + */ +import { useEffect, useLayoutEffect, useRef, version } from 'react'; +import type { CSSProperties, HTMLAttributes, ReactNode, RefObject } from 'react'; +import './device-frames.css'; + +export type MacBookColor = 'silver' | 'space-black'; +export type IPhoneColor = 'black' | 'natural' | 'white' | 'desert'; + +// useLayoutEffect warnt beim Server-Rendering, dort reicht useEffect (läuft nicht) +const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +/** Passt eine Canvas in Designgröße per transform: scale() an ihren Viewport an. */ +function useCanvasScale(ref: RefObject, width: number, height: number): void { + useIsoLayoutEffect(() => { + const canvas = ref.current; + const host = canvas ? canvas.parentElement : null; + if (!canvas || !host) return undefined; + const apply = (w: number) => { + if (!w) return; // noch nicht sichtbar + const k = Math.round((w / width) * 1e6) / 1e6; + canvas.style.setProperty('--dm-dw', String(width)); + canvas.style.setProperty('--dm-dh', String(height)); + canvas.style.setProperty('--dm-k', String(k)); + }; + // Bruchteil-genaue Breite (clientWidth rundet) + apply(parseFloat(getComputedStyle(host).width) || host.clientWidth); + if (typeof ResizeObserver !== 'function') return undefined; + const ro = new ResizeObserver((entries) => { + const last = entries[entries.length - 1]; + if (last) apply(last.contentRect.width); + }); + ro.observe(host); + return () => ro.disconnect(); + }, [ref, width, height]); +} + +// inert: React 19 kennt es als boolesches Attribut (dort hieße '' "aus"), +// React 18 reicht es als unbekanntes Attribut nur als String durch ('' = gesetzt). +const INERT = (parseInt(version, 10) >= 19 ? { inert: true } : { inert: '' }) as unknown as { inert?: boolean }; + +/** decorative: Gerät ist ein Bild (role="img"), Inhalt nicht fokussier- oder klickbar */ +function DeviceCanvas({ width, height, decorative, children }: { + width: number; + height: number; + decorative?: boolean; + children?: ReactNode; +}) { + const ref = useRef(null); + useCanvasScale(ref, width, height); + return ( +
+ {children} +
+ ); +} + +/** Attribute, die an das Wurzelelement weitergereicht werden (id, aria-hidden, data-*, onClick …) */ +type RootAttributes = Omit, 'children' | 'role' | 'color' | 'className' | 'style'>; + +const cx = (...parts: Array) => parts.filter(Boolean).join(' '); + +/* ------------------------------------------------------------ Symbole + Alle dekorativ (aria-hidden), gezeichnet in Design-Punkten. */ +const G = { + // Ampel als eine Grafik: drei exakt gleiche Kreise bei jeder Größe + lights: ( + + ), + apple: ( + + ), + macBattery: ( + + ), + wifi: ( + + ), + search: ( + + ), + control: ( + + ), + sidebar: ( + + ), + chevDown: ( + + ), + back: ( + + ), + fwd: ( + + ), + pageMenu: ( + + ), + lock: ( + + ), + reload: ( + + ), + share: ( + + ), + plus: ( + + ), + tabs: ( + + ), + cellular: ( + + ), + iosBattery: ( + + ), +}; + +const MENUS = ['Ablage', 'Bearbeiten', 'Darstellung', 'Verlauf', 'Lesezeichen', 'Fenster', 'Hilfe']; + +/* ------------------------------------------------------------ MacBook */ +export interface MacBookFrameProps extends RootAttributes { + /** Adresse in der Safari-Adressleiste */ + url?: string; + color?: MacBookColor; + /** false = Vollbild ohne macOS-Menüleiste (Streifen neben der Notch schwarz, keine Fenstertasten) */ + menuBar?: boolean; + /** Datum und Uhrzeit rechts in der Menüleiste */ + clock?: string; + className?: string; + style?: CSSProperties; + /** Setzt role="img" + aria-label; der Inhalt ist dann ein Bild (inert, nicht bedienbar) */ + label?: string; + /** Inhalt in 1512 x 893 Design-Pixeln */ + children?: ReactNode; +} + +export function MacBookFrame({ + url = 'project.webklar.com', + color = 'silver', + menuBar = true, + clock = 'Do. 1. Okt. 09:41', + className, + style, + label, + children, + ...rest +}: MacBookFrameProps) { + return ( +
+
+
+
+ {menuBar && ( + + )} +
+ +
+ + {children} + +
+
+ +
+
+
+