fix: Move EWR toggle to banner header with info button
Build + Deploy / build-backend-compliance (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 23s
Build + Deploy / build-admin-compliance (push) Successful in 2m9s
Build + Deploy / build-ai-sdk (push) Successful in 8s
Build + Deploy / build-developer-portal (push) Successful in 7s
Build + Deploy / build-tts (push) Successful in 11s
Build + Deploy / build-document-crawler (push) Successful in 7s
Build + Deploy / build-dsms-gateway (push) Successful in 7s
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / loc-budget (push) Failing after 15s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
Build + Deploy / trigger-orca (push) Successful in 2m26s
CI / validate-canonical-controls (push) Successful in 14s

- EWR toggle now visible on initial banner view (top-right, always visible)
- Info button (i) with tooltip explaining EWR-only mode
- Blocked vendors count badge below toggle
- Blocked vendor pills shown below header text
- Removed duplicate EWR section from settings view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-02 22:18:45 +02:00
parent 9bc816e55c
commit 9510ce0ff9
@@ -125,6 +125,8 @@ export function CookieBannerOverlay() {
<div className="max-w-3xl mx-auto m-4 bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
{/* Header */}
<div className="px-6 pt-6 pb-4">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
@@ -133,8 +135,31 @@ export function CookieBannerOverlay() {
</h2>
<p className="text-sm text-gray-600 mt-2 leading-relaxed">
Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Erfahrung zu bieten.
Sie koennen Ihre Praeferenzen jederzeit aendern.
</p>
</div>
{/* EWR-Only Toggle — always visible in header */}
<EWRToggle
checked={consent.ewrOnly}
onChange={() => setConsent(prev => ({ ...prev, ewrOnly: !prev.ewrOnly }))}
blockedCount={blockedVendors.length}
/>
</div>
{/* Blocked vendors pills */}
{consent.ewrOnly && blockedVendors.length > 0 && (
<div className="mt-3 flex flex-wrap gap-1">
{blockedVendors.map(name => (
<span key={name} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-50 text-red-600 text-[10px] font-medium border border-red-100">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
{name}
</span>
))}
</div>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
<a href="/sdk/einwilligungen/cookie-banner" className="hover:text-purple-600 underline">
Datenschutzerklaerung
@@ -149,51 +174,6 @@ export function CookieBannerOverlay() {
{/* Settings */}
{showSettings && (
<div className="border-t border-gray-100">
{/* === DRITTLAND-SCHUTZ === */}
<div className="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-blue-100">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center shrink-0 mt-0.5">
<svg className="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div className="text-sm font-semibold text-blue-900">
Nur EU/EWR-Anbieter
</div>
<p className="text-xs text-blue-700 mt-0.5 leading-relaxed">
Erlaubt nur Anbieter mit Sitz im Europaeischen Wirtschaftsraum (EWR) oder
der Schweiz. Anbieter ausserhalb werden blockiert auch wenn Sie einer
Kategorie zustimmen. {nonEWRCount} Anbieter betroffen.
</p>
{consent.ewrOnly && blockedVendors.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{blockedVendors.map(name => (
<span key={name} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-[10px] font-medium">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
{name}
</span>
))}
</div>
)}
</div>
</div>
<button
onClick={() => setConsent(prev => ({ ...prev, ewrOnly: !prev.ewrOnly }))}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 mt-1 ${
consent.ewrOnly ? 'bg-blue-600' : 'bg-gray-200'
} cursor-pointer`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
consent.ewrOnly ? 'translate-x-6' : 'translate-x-1'
}`} />
</button>
</div>
</div>
{/* Category Sections */}
<div className="px-6 py-4 space-y-1 max-h-[40vh] overflow-y-auto">
{Object.entries(CATEGORY_VENDORS).map(([key, cat]) => (
@@ -430,3 +410,81 @@ function CategorySection({
</div>
)
}
function EWRToggle({
checked,
onChange,
blockedCount,
}: {
checked: boolean
onChange: () => void
blockedCount: number
}) {
const [showInfo, setShowInfo] = useState(false)
return (
<div className="flex flex-col items-end gap-1.5 shrink-0">
{/* Toggle Row */}
<div className="flex items-center gap-2">
{/* Info Button */}
<button
onClick={() => setShowInfo(!showInfo)}
className="w-5 h-5 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 flex items-center justify-center text-xs font-bold transition-colors"
aria-label="Info zu Nur EU/EWR"
>
i
</button>
<span className={`text-xs font-medium whitespace-nowrap ${checked ? 'text-blue-700' : 'text-gray-500'}`}>
Nur EU/EWR
</span>
<button
onClick={onChange}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 cursor-pointer ${
checked ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
checked ? 'translate-x-6' : 'translate-x-1'
}`} />
</button>
</div>
{/* Blocked count badge */}
{checked && blockedCount > 0 && (
<span className="text-[10px] text-red-600 font-medium">
{blockedCount} Anbieter blockiert
</span>
)}
{/* Info Tooltip */}
{showInfo && (
<div className="absolute right-6 top-16 w-72 p-3 bg-blue-50 border border-blue-200 rounded-lg shadow-lg z-10 text-xs text-blue-800 leading-relaxed">
<div className="font-semibold mb-1 flex items-center gap-1.5">
<svg className="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Nur EU/EWR-Anbieter
</div>
<p>
Erlaubt nur Datenverarbeitung durch Anbieter mit Sitz im Europaeischen
Wirtschaftsraum (EU + Island, Liechtenstein, Norwegen) oder der Schweiz.
</p>
<p className="mt-1.5">
Anbieter ausserhalb (z.B. USA) werden blockiert auch wenn Sie einer
Cookie-Kategorie zustimmen. So behalten Sie die volle Kontrolle ueber
internationale Datentransfers.
</p>
<button
onClick={() => setShowInfo(false)}
className="mt-2 text-blue-600 hover:text-blue-800 font-medium"
>
Verstanden
</button>
</div>
)}
</div>
)
}