Files
ClaudeandClaude Opus 5.5 0a113c2983 Heatmap in Geraeterahmen (Computer/Tablet + Handy) und Keks statt "Datenschutz-Einstellungen"
Heatmap im Portal (Funktionen -> Analyse): die echte Website laeuft jetzt in realistischen
Geraeterahmen, gross MacBook oder iPad (Umschalter oben), daneben das iPhone, jeweils in echter
Breite (1512 / 820 / 402 px), verkleinert und im Geraet scrollbar, mit den Klicks der
passenden Geraeteklasse als Overlay. MacBook und iPhone stammen aus den Rahmen der neuen
Website (device-mockups), das iPad ist im gleichen Stil nachgebaut (public/geraete.css/.js).

- heatmap-link: embed: true haengt __wk_embed=1 an; w.js reicht das als data-embed an h.js.
- h.js eingebettet (nur in einem Rahmen): keine Leiste, Links und Formulare gesperrt (kein
  Seitenwechsel, also keine Zaehlung und kein Banner), Scrollleisten aus, Klickzahl per
  postMessage nur an die Herkunft von h.js (project.webklar.com).
- iframe mit Sandbox allow-scripts allow-same-origin (keine Formulare, Pop-ups, keine
  Navigation des Portals); Laden erst, wenn die Heatmap in Sicht kommt. Laesst sich eine
  Website nicht einbetten, kommt nach 20 s ein Hinweis mit "In neuem Tab oeffnen" (alter Weg).

Banner-Skript: der Einstellungsknopf nach der Wahl ist jetzt ein angeknabberter Keks unten
rechts (Inline-SVG, 44 px). Er dreht sich langsam, haelt beim Darueberfahren/Fokus an und
zeigt dann "Datenschutz-Einstellungen" (auch sein Name fuer Screenreader, passend zum
Bannertext); bei reduzierter Bewegung dreht er sich nicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:09:17 +00:00

130 lines
12 KiB
JavaScript

/* Geraeterahmen fuer die Heatmap-Ansicht (Analyse): MacBook Pro 14", iPad und iPhone 16 Pro.
MacBook und iPhone stammen aus den Rahmen der neuen WEBklar-Website (device-mockups, KNSONWS/homepage),
das iPad ist im gleichen Stil nachgebaut. Styles: geraete.css. Reine Zeichenketten ohne DOM, damit Node-Tests sie pruefen koennen. */
// Je Geraeteklasse (wie vpClass in analytics/client/core.js: m < 768 <= t < 1024 <= d) die Bildschirmflaeche des Rahmens (canvas)
// und darin die Website (site: unter der Statusleiste, in echter Breite).
export const HEAT_DEVICES = {
d: { label: 'Computer', frame: 'macbook', canvas: [1512, 893], site: { top: 0, width: 1512, height: 893 } },
t: { label: 'Tablet', frame: 'ipad', canvas: [820, 1180], site: { top: 24, width: 820, height: 1156 } },
m: { label: 'Handy', frame: 'iphone', canvas: [402, 874], site: { top: 59, width: 402, height: 815 } },
}
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
/** Uhrzeit wie in Apple-Werbebildern (9:41), Datum von heute: { mac: 'Fr. 2. Okt. 9:41', ipad: '9:41 Fr. 2. Okt.' } */
export function deviceClock(date = new Date()) {
// "Fr., 2. Okt." => "Fr. 2. Okt." (wie die macOS-Menueleiste)
const day = new Intl.DateTimeFormat('de-DE', { weekday: 'short', day: 'numeric', month: 'short' }).format(date).replace(',', '')
return { mac: `${day} 9:41`, ipad: `9:41 ${day}` }
}
/** Untertitel unter einem Geraet je Zustand (loading | ready | error | timeout). */
export function heatCaption(state, count = 0, format = (n) => String(n)) {
if (state === 'ready') {
if (!(count > 0)) return 'Noch keine Klicks auf dieser Seite (letzte 30 Tage)'
return `${format(count)} ${count === 1 ? 'Klick' : 'Klicks'} · letzte 30 Tage`
}
if (state === 'error') return 'Die Heatmap konnte nicht geladen werden.'
if (state === 'timeout') return 'Deine Website lässt sich hier nicht anzeigen.'
return 'Lädt …'
}
/** Adresszeile im MacBook-Safari: Host + Pfad, ohne https:// und ohne Parameter. */
export function addressOf(url) {
try {
const u = new URL(url)
return u.host + (u.pathname === '/' ? '' : u.pathname)
} catch {
return ''
}
}
/** Website im Rahmen: iframe in echter Breite unter der Statusleiste. Sandbox ohne Formulare, Pop-ups und Navigation des Portals. */
function siteHtml(vp, src, title) {
const s = HEAT_DEVICES[vp].site
return `<div class="hs-site" style="top:${s.top}px;width:${s.width}px;height:${s.height}px"><iframe src="${esc(src)}" title="${esc(title)}" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer"></iframe></div>`
}
function canvasHtml(vp, inner) {
const [w, h] = HEAT_DEVICES[vp].canvas
return `<div class="dm-canvas" data-design-width="${w}" data-design-height="${h}" style="--dm-dw:${w};--dm-dh:${h}">${inner}</div>`
}
const MENUS = ['Ablage', 'Bearbeiten', 'Darstellung', 'Verlauf', 'Lesezeichen', 'Fenster', 'Hilfe']
/** Rahmen fuer Geraeteklasse vp ('d' | 't' | 'm') mit der Website src. title = Name des iframes fuer Screenreader. */
export function deviceHtml(vp, { src, title = '', clock = deviceClock() } = {}) {
const dev = HEAT_DEVICES[vp]
if (!dev) return ''
const canvas = canvasHtml(vp, siteHtml(vp, src, title))
if (dev.frame === 'macbook') {
return `<div class="dm-macbook" data-color="silver">
<div class="dm-macbook__lid"><div class="dm-macbook__glass"><div class="dm-macbook__screen">
<div class="dm-macbook__menubar" aria-hidden="true">
<div class="dm-macbook__menus">${ICON.apple}<b>Safari</b>${MENUS.map((m) => `<span>${m}</span>`).join('')}</div>
<div class="dm-macbook__extras">${ICON.macBattery}${ICON.wifi}${ICON.search}${ICON.control}<span>${esc(clock.mac)}</span></div>
</div>
<div class="dm-macbook__window">
<div class="dm-macbook__toolbar" aria-hidden="true">
<span class="dm-macbook__lights">${ICON.lights}</span>
<span class="dm-macbook__tool dm-macbook__tool--sidebar">${ICON.sidebar}${ICON.chevDown}</span>
<span class="dm-macbook__tool dm-macbook__tool--nav">${ICON.back}${ICON.fwd}</span>
<span class="dm-macbook__address">${ICON.pageMenu}<span class="dm-macbook__url">${ICON.lock}<span>${esc(addressOf(src))}</span></span>${ICON.reload}</span>
<span class="dm-macbook__tool dm-macbook__tool--right">${ICON.share}${ICON.plus}${ICON.tabs}</span>
</div>
<div class="dm-macbook__viewport">${canvas}</div>
</div>
<div class="dm-macbook__notch" aria-hidden="true"><i class="dm-macbook__camera"></i></div>
</div></div></div>
<div class="dm-macbook__hinge" aria-hidden="true"></div>
<div class="dm-macbook__base" aria-hidden="true"><i class="dm-macbook__lip"></i></div>
<div class="dm-macbook__shadow" aria-hidden="true"></div>
</div>`
}
if (dev.frame === 'ipad') {
return `<div class="dm-ipad" data-color="silver">
<span class="dm-ipad__buttons" aria-hidden="true"><i class="dm-ipad__btn--top"></i><i class="dm-ipad__btn--vol-up"></i><i class="dm-ipad__btn--vol-down"></i></span>
<div class="dm-ipad__body"><div class="dm-ipad__glass">
<i class="dm-ipad__camera" aria-hidden="true"></i>
<div class="dm-ipad__screen">
<div class="dm-ipad__viewport">${canvas}</div>
<div class="dm-ipad__statusbar" aria-hidden="true"><span class="dm-ipad__time">${esc(clock.ipad)}</span><span class="dm-ipad__indicators">${ICON.wifi}${ICON.iosBattery}</span></div>
<div class="dm-ipad__home" aria-hidden="true"></div>
</div>
</div></div>
</div>`
}
return `<div class="dm-iphone" data-color="natural" data-statusbar="dark">
<span class="dm-iphone__buttons" aria-hidden="true"><i class="dm-iphone__btn--action"></i><i class="dm-iphone__btn--vol-up"></i><i class="dm-iphone__btn--vol-down"></i><i class="dm-iphone__btn--side"></i><i class="dm-iphone__btn--camera"></i></span>
<div class="dm-iphone__body"><div class="dm-iphone__glass"><div class="dm-iphone__screen">
<div class="dm-iphone__viewport">${canvas}</div>
<div class="dm-iphone__statusbar" aria-hidden="true"><span class="dm-iphone__time">9:41</span><span class="dm-iphone__indicators">${ICON.cellular}${ICON.wifi}${ICON.iosBattery}</span></div>
<div class="dm-iphone__island" aria-hidden="true"><i></i></div>
<div class="dm-iphone__home" aria-hidden="true"></div>
</div></div></div>
</div>`
}
/* Symbole der Rahmen (aus DeviceFrames.tsx, alle dekorativ) */
const ICON = {
lights: '<svg viewBox="0 0 52 12" aria-hidden="true" focusable="false"><circle cx="6" cy="6" r="5.7" fill="#ff5f57" stroke="#000" stroke-opacity=".22" stroke-width=".6" /><circle cx="26" cy="6" r="5.7" fill="#febc2e" stroke="#000" stroke-opacity=".22" stroke-width=".6" /><circle cx="46" cy="6" r="5.7" fill="#28c840" stroke="#000" stroke-opacity=".22" stroke-width=".6" /></svg>',
apple: '<svg class="dm-macbook__apple" viewBox="0 0 14 17" aria-hidden="true" focusable="false"><path fill="currentColor" d="M11.62 9.03c-.02-1.9 1.55-2.82 1.62-2.86-.88-1.29-2.26-1.47-2.75-1.49-1.17-.12-2.28.69-2.88.69-.59 0-1.5-.67-2.47-.65A3.67 3.67 0 0 0 2.05 6.6C.73 8.88 1.71 12.27 3 14.13c.63.91 1.38 1.93 2.36 1.9.95-.04 1.31-.61 2.45-.61 1.15 0 1.47.61 2.47.59 1.02-.02 1.66-.93 2.29-1.84.72-1.06 1.02-2.08 1.03-2.13-.02-.01-1.97-.76-1.98-3.01ZM9.75 3.46c.52-.63.87-1.51.78-2.38-.75.03-1.66.5-2.2 1.13-.48.56-.9 1.45-.79 2.31.84.07 1.69-.42 2.21-1.06Z" /></svg>',
macBattery: '<svg viewBox="0 0 27 13" aria-hidden="true" focusable="false"><rect x=".6" y=".6" width="23.2" height="11.8" rx="3.4" fill="none" stroke="currentColor" stroke-opacity=".45" stroke-width="1.2" /><rect x="2.3" y="2.3" width="16.4" height="8.4" rx="1.9" fill="currentColor" /><path d="M25 4.4c.9.2 1.5.9 1.5 2.1s-.6 1.9-1.5 2.1z" fill="currentColor" fill-opacity=".45" /></svg>',
wifi: '<svg viewBox="0 0 17 12.6" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="2.1"><path d="M1.43 4.93a10 10 0 0 1 14.14 0" /><path d="M4.05 7.55a6.3 6.3 0 0 1 8.9 0" /></g><path fill="currentColor" d="M8.5 12 5.95 9.45a3.6 3.6 0 0 1 5.1 0Z" /></svg>',
search: '<svg viewBox="0 0 15 15" aria-hidden="true" focusable="false"><circle cx="6.2" cy="6.2" r="4.9" fill="none" stroke="currentColor" stroke-width="1.6" /><path d="m9.8 9.8 4.2 4.2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>',
control: '<svg viewBox="0 0 17 14" aria-hidden="true" focusable="false"><rect x=".8" y=".8" width="15.4" height="5.2" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.3" /><circle cx="4.1" cy="3.4" r="1.7" fill="currentColor" /><rect x=".8" y="8" width="15.4" height="5.2" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.3" /><circle cx="12.9" cy="10.6" r="1.7" fill="currentColor" /></svg>',
sidebar: '<svg viewBox="0 0 22 17" aria-hidden="true" focusable="false"><rect x=".8" y=".8" width="20.4" height="15.4" rx="3.4" fill="none" stroke="currentColor" stroke-width="1.5" /><path d="M8 1v15" stroke="currentColor" stroke-width="1.5" /><path d="M3.3 4.6h2.4M3.3 7.1h2.4M3.3 9.6h2.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" /></svg>',
chevDown: '<svg viewBox="0 0 9 6" aria-hidden="true" focusable="false"><path d="m1 1 3.5 3.5L8 1" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" /></svg>',
back: '<svg viewBox="0 0 10 17" aria-hidden="true" focusable="false"><path d="M8.3 1.4 1.7 8.5l6.6 7.1" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" /></svg>',
fwd: '<svg viewBox="0 0 10 17" aria-hidden="true" focusable="false"><path d="m1.7 1.4 6.6 7.1-6.6 7.1" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" /></svg>',
pageMenu: '<svg viewBox="0 0 18 14" aria-hidden="true" focusable="false"><rect x=".8" y=".8" width="16.4" height="12.4" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.3" /><path d="M4 4.6h10M4 7h10M4 9.4h6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" /></svg>',
lock: '<svg viewBox="0 0 10 13" aria-hidden="true" focusable="false"><path d="M2.6 5.6V4a2.4 2.4 0 0 1 4.8 0v1.6" fill="none" stroke="currentColor" stroke-width="1.35" /><rect x=".6" y="5.4" width="8.8" height="7" rx="1.6" fill="currentColor" /></svg>',
reload: '<svg viewBox="0 0 14 15" aria-hidden="true" focusable="false"><path d="M12 8.4A5 5 0 1 1 9.7 3.6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /><path d="M8.4 1 11.2 3.5 8.2 5.9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>',
share: '<svg viewBox="0 0 16 20" aria-hidden="true" focusable="false"><path d="M5.2 7.2H3.4A1.9 1.9 0 0 0 1.5 9.1v7.5c0 1.05.85 1.9 1.9 1.9h9.2c1.05 0 1.9-.85 1.9-1.9V9.1a1.9 1.9 0 0 0-1.9-1.9h-1.8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /><path d="M8 12.2V1.4M4.8 4.4 8 1.2l3.2 3.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>',
plus: '<svg viewBox="0 0 15 15" aria-hidden="true" focusable="false"><path d="M7.5 1v13M1 7.5h13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>',
tabs: '<svg viewBox="0 0 18 18" aria-hidden="true" focusable="false"><rect x="4.8" y=".9" width="12.3" height="12.3" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.4" /><path d="M12.9 15.3a2.4 2.4 0 0 1-2.2 1.8H3.4A2.5 2.5 0 0 1 .9 14.6V7.3a2.4 2.4 0 0 1 1.8-2.3" fill="none" stroke="currentColor" stroke-width="1.4" /></svg>',
cellular: '<svg viewBox="0 0 18 12" aria-hidden="true" focusable="false"><rect x="0" y="7.4" width="3.1" height="4.6" rx=".9" fill="currentColor" /><rect x="4.9" y="5" width="3.1" height="7" rx=".9" fill="currentColor" /><rect x="9.8" y="2.5" width="3.1" height="9.5" rx=".9" fill="currentColor" /><rect x="14.7" y="0" width="3.1" height="12" rx=".9" fill="currentColor" /></svg>',
iosBattery: '<svg viewBox="0 0 27.5 13" aria-hidden="true" focusable="false"><rect x=".55" y=".55" width="23.9" height="11.9" rx="3.7" fill="none" stroke="currentColor" stroke-opacity=".38" stroke-width="1.1" /><rect x="2.1" y="2.1" width="18.6" height="8.8" rx="2.2" fill="currentColor" /><path d="M25.6 4.3c.95.25 1.5 1.05 1.5 2.2s-.55 1.95-1.5 2.2z" fill="currentColor" fill-opacity=".4" /></svg>',
}