Initial commit: breakpilot-compliance - Compliance SDK Platform

Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,611 @@
/**
* BreakPilot Compliance SDK - Embed Script
*
* Usage:
* <script src="https://cdn.breakpilot.app/sdk/v1/compliance.min.js"></script>
* <script>
* BreakPilotSDK.init({
* apiEndpoint: 'https://compliance.example.com/api/v1',
* apiKey: 'pk_live_xxx',
* autoInjectBanner: true,
* bannerConfig: { position: 'bottom', theme: 'dark', language: 'de' }
* });
* </script>
*/
import { ComplianceClient } from '@breakpilot/compliance-sdk-core'
import type {
ConsentPurpose,
CookieBannerPosition,
CookieBannerTheme,
DSRRequestType,
} from '@breakpilot/compliance-sdk-types'
// ============================================================================
// Types
// ============================================================================
export interface BreakPilotSDKConfig {
apiEndpoint: string
apiKey: string
tenantId?: string
autoInjectBanner?: boolean
bannerConfig?: BannerConfig
onConsentChange?: (consents: Record<ConsentPurpose, boolean>) => void
onReady?: () => void
onError?: (error: Error) => void
debug?: boolean
}
export interface BannerConfig {
position?: CookieBannerPosition
theme?: CookieBannerTheme
language?: 'de' | 'en'
privacyPolicyUrl?: string
imprintUrl?: string
texts?: {
title?: string
description?: string
acceptAll?: string
rejectAll?: string
settings?: string
save?: string
}
customColors?: {
background?: string
text?: string
primary?: string
secondary?: string
}
}
// ============================================================================
// Internal State
// ============================================================================
let _client: ComplianceClient | null = null
let _config: BreakPilotSDKConfig | null = null
let _consents: Record<ConsentPurpose, boolean> = {
ESSENTIAL: true,
FUNCTIONAL: false,
ANALYTICS: false,
MARKETING: false,
PERSONALIZATION: false,
THIRD_PARTY: false,
}
let _bannerElement: HTMLElement | null = null
let _isInitialized = false
// ============================================================================
// Translations
// ============================================================================
const TRANSLATIONS = {
de: {
title: 'Cookie-Einwilligung',
description:
'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.',
acceptAll: 'Alle akzeptieren',
rejectAll: 'Nur notwendige',
settings: 'Einstellungen',
save: 'Speichern',
privacy: 'Datenschutz',
imprint: 'Impressum',
categories: {
ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' },
FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' },
ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' },
MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' },
PERSONALIZATION: { name: 'Personalisierung', description: 'Angepasste Inhalte' },
THIRD_PARTY: { name: 'Drittanbieter', description: 'Externe Dienste' },
},
},
en: {
title: 'Cookie Consent',
description:
'We use cookies to improve your experience. For more information, please see our privacy policy.',
acceptAll: 'Accept All',
rejectAll: 'Reject Non-Essential',
settings: 'Settings',
save: 'Save',
privacy: 'Privacy Policy',
imprint: 'Imprint',
categories: {
ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' },
FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' },
ANALYTICS: { name: 'Analytics', description: 'Usage statistics' },
MARKETING: { name: 'Marketing', description: 'Personalized advertising' },
PERSONALIZATION: { name: 'Personalization', description: 'Customized content' },
THIRD_PARTY: { name: 'Third Party', description: 'External services' },
},
},
}
// ============================================================================
// Utility Functions
// ============================================================================
function log(message: string, ...args: unknown[]): void {
if (_config?.debug) {
console.log(`[BreakPilotSDK] ${message}`, ...args)
}
}
function getStoredConsents(): Record<ConsentPurpose, boolean> | null {
try {
const stored = localStorage.getItem('breakpilot_consents')
return stored ? JSON.parse(stored) : null
} catch {
return null
}
}
function storeConsents(consents: Record<ConsentPurpose, boolean>): void {
try {
localStorage.setItem('breakpilot_consents', JSON.stringify(consents))
localStorage.setItem('breakpilot_consents_timestamp', Date.now().toString())
} catch {
log('Failed to store consents')
}
}
function createElement<K extends keyof HTMLElementTagNameMap>(
tag: K,
styles: Partial<CSSStyleDeclaration> = {},
attributes: Record<string, string> = {}
): HTMLElementTagNameMap[K] {
const el = document.createElement(tag)
Object.assign(el.style, styles)
Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value))
return el
}
// ============================================================================
// Banner Implementation
// ============================================================================
function createBanner(): HTMLElement {
const config = _config!
const bannerConfig = config.bannerConfig || {}
const lang = bannerConfig.language || 'de'
const t = TRANSLATIONS[lang]
const position = bannerConfig.position || 'BOTTOM'
const theme = bannerConfig.theme || 'LIGHT'
const isDark = theme === 'DARK'
const bgColor = bannerConfig.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff')
const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a')
const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a')
const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a')
// Container
const container = createElement(
'div',
{
position: 'fixed',
zIndex: '99999',
left: position === 'CENTER' ? '50%' : '0',
right: position === 'CENTER' ? 'auto' : '0',
top: position === 'TOP' ? '0' : position === 'CENTER' ? '50%' : 'auto',
bottom: position === 'BOTTOM' ? '0' : 'auto',
transform: position === 'CENTER' ? 'translate(-50%, -50%)' : 'none',
maxWidth: position === 'CENTER' ? '500px' : 'none',
backgroundColor: bgColor,
color: textColor,
padding: '20px',
boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontSize: '14px',
lineHeight: '1.5',
},
{ id: 'breakpilot-consent-banner', role: 'dialog', 'aria-label': t.title }
)
// Title
const title = createElement('h3', {
margin: '0 0 10px',
fontSize: '18px',
fontWeight: '600',
})
title.textContent = bannerConfig.texts?.title || t.title
// Description
const description = createElement('p', {
margin: '0 0 15px',
opacity: '0.8',
})
description.textContent = bannerConfig.texts?.description || t.description
// Buttons container
const buttonsContainer = createElement('div', {
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
alignItems: 'center',
})
// Accept All button
const acceptBtn = createElement(
'button',
{
padding: '10px 20px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
fontWeight: '500',
backgroundColor: primaryColor,
color: isDark ? '#1a1a1a' : '#ffffff',
},
{ type: 'button' }
)
acceptBtn.textContent = bannerConfig.texts?.acceptAll || t.acceptAll
acceptBtn.onclick = () => handleAcceptAll()
// Reject button
const rejectBtn = createElement(
'button',
{
padding: '10px 20px',
borderRadius: '4px',
backgroundColor: 'transparent',
border: `1px solid ${secondaryColor}`,
color: textColor,
cursor: 'pointer',
fontWeight: '500',
},
{ type: 'button' }
)
rejectBtn.textContent = bannerConfig.texts?.rejectAll || t.rejectAll
rejectBtn.onclick = () => handleRejectAll()
// Settings button
const settingsBtn = createElement(
'button',
{
padding: '10px 20px',
borderRadius: '4px',
backgroundColor: 'transparent',
border: `1px solid ${secondaryColor}`,
color: textColor,
cursor: 'pointer',
fontWeight: '500',
},
{ type: 'button' }
)
settingsBtn.textContent = bannerConfig.texts?.settings || t.settings
settingsBtn.onclick = () => showSettingsPanel()
// Links container
const linksContainer = createElement('div', {
marginLeft: 'auto',
fontSize: '12px',
})
const privacyLink = createElement('a', {
marginRight: '15px',
color: textColor,
textDecoration: 'none',
})
privacyLink.href = bannerConfig.privacyPolicyUrl || '/privacy'
privacyLink.textContent = t.privacy
const imprintLink = createElement('a', {
color: textColor,
textDecoration: 'none',
})
imprintLink.href = bannerConfig.imprintUrl || '/imprint'
imprintLink.textContent = t.imprint
// Assemble
linksContainer.appendChild(privacyLink)
linksContainer.appendChild(imprintLink)
buttonsContainer.appendChild(acceptBtn)
buttonsContainer.appendChild(rejectBtn)
buttonsContainer.appendChild(settingsBtn)
buttonsContainer.appendChild(linksContainer)
container.appendChild(title)
container.appendChild(description)
container.appendChild(buttonsContainer)
return container
}
function showSettingsPanel(): void {
if (!_bannerElement) return
const config = _config!
const bannerConfig = config.bannerConfig || {}
const lang = bannerConfig.language || 'de'
const t = TRANSLATIONS[lang]
const theme = bannerConfig.theme || 'LIGHT'
const isDark = theme === 'DARK'
const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a')
// Clear banner content
_bannerElement.innerHTML = ''
// Title
const title = createElement('h3', {
margin: '0 0 15px',
fontSize: '18px',
fontWeight: '600',
})
title.textContent = t.settings
_bannerElement.appendChild(title)
// Categories
const categories: ConsentPurpose[] = [
'ESSENTIAL',
'FUNCTIONAL',
'ANALYTICS',
'MARKETING',
]
categories.forEach(category => {
const catInfo = t.categories[category]
const row = createElement('div', {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 0',
borderBottom: `1px solid ${isDark ? '#333' : '#eee'}`,
})
const labelContainer = createElement('div', {})
const labelName = createElement('div', { fontWeight: '500' })
labelName.textContent = catInfo.name
const labelDesc = createElement('div', { fontSize: '12px', opacity: '0.7' })
labelDesc.textContent = catInfo.description
labelContainer.appendChild(labelName)
labelContainer.appendChild(labelDesc)
const checkbox = createElement(
'input',
{
width: '20px',
height: '20px',
cursor: category === 'ESSENTIAL' ? 'not-allowed' : 'pointer',
},
{
type: 'checkbox',
'data-category': category,
}
)
checkbox.checked = _consents[category]
checkbox.disabled = category === 'ESSENTIAL'
checkbox.onchange = () => {
if (category !== 'ESSENTIAL') {
_consents[category] = checkbox.checked
}
}
row.appendChild(labelContainer)
row.appendChild(checkbox)
_bannerElement!.appendChild(row)
})
// Buttons
const buttonsContainer = createElement('div', {
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
marginTop: '15px',
})
const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a')
const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a')
const saveBtn = createElement(
'button',
{
padding: '10px 20px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
fontWeight: '500',
backgroundColor: primaryColor,
color: isDark ? '#1a1a1a' : '#ffffff',
},
{ type: 'button' }
)
saveBtn.textContent = bannerConfig.texts?.save || t.save
saveBtn.onclick = () => handleSaveSettings()
const backBtn = createElement(
'button',
{
padding: '10px 20px',
borderRadius: '4px',
backgroundColor: 'transparent',
border: `1px solid ${secondaryColor}`,
color: textColor,
cursor: 'pointer',
fontWeight: '500',
},
{ type: 'button' }
)
backBtn.textContent = 'Zurück'
backBtn.onclick = () => showMainBanner()
buttonsContainer.appendChild(saveBtn)
buttonsContainer.appendChild(backBtn)
_bannerElement.appendChild(buttonsContainer)
}
function showMainBanner(): void {
if (_bannerElement) {
_bannerElement.remove()
}
_bannerElement = createBanner()
document.body.appendChild(_bannerElement)
}
function hideBanner(): void {
if (_bannerElement) {
_bannerElement.remove()
_bannerElement = null
}
}
// ============================================================================
// Consent Handlers
// ============================================================================
function handleAcceptAll(): void {
_consents = {
ESSENTIAL: true,
FUNCTIONAL: true,
ANALYTICS: true,
MARKETING: true,
PERSONALIZATION: true,
THIRD_PARTY: true,
}
applyConsents()
}
function handleRejectAll(): void {
_consents = {
ESSENTIAL: true,
FUNCTIONAL: false,
ANALYTICS: false,
MARKETING: false,
PERSONALIZATION: false,
THIRD_PARTY: false,
}
applyConsents()
}
function handleSaveSettings(): void {
applyConsents()
}
function applyConsents(): void {
storeConsents(_consents)
hideBanner()
_config?.onConsentChange?.(_consents)
log('Consents applied:', _consents)
}
// ============================================================================
// Public API
// ============================================================================
function init(config: BreakPilotSDKConfig): void {
if (_isInitialized) {
log('SDK already initialized')
return
}
_config = config
log('Initializing with config:', config)
try {
_client = new ComplianceClient({
apiEndpoint: config.apiEndpoint,
apiKey: config.apiKey,
tenantId: config.tenantId,
})
// Check for stored consents
const storedConsents = getStoredConsents()
if (storedConsents) {
_consents = storedConsents
log('Loaded stored consents:', _consents)
config.onConsentChange?.(storedConsents)
} else if (config.autoInjectBanner !== false) {
// Show banner if no stored consents
showMainBanner()
}
_isInitialized = true
config.onReady?.()
log('SDK initialized successfully')
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
config.onError?.(err)
log('SDK initialization failed:', err)
}
}
function getConsents(): Record<ConsentPurpose, boolean> {
return { ..._consents }
}
function updateConsent(purpose: ConsentPurpose, granted: boolean): void {
if (purpose === 'ESSENTIAL') {
log('Cannot change essential consent')
return
}
_consents[purpose] = granted
storeConsents(_consents)
_config?.onConsentChange?.(_consents)
log('Consent updated:', purpose, granted)
}
function showBanner(): void {
showMainBanner()
}
function hasConsent(purpose: ConsentPurpose): boolean {
return _consents[purpose]
}
function getClient(): ComplianceClient | null {
return _client
}
async function submitDSR(
type: DSRRequestType,
email: string,
name: string
): Promise<{ success: boolean; requestId?: string; error?: string }> {
if (!_client) {
return { success: false, error: 'SDK not initialized' }
}
try {
// This would call the DSR endpoint
log('Submitting DSR:', { type, email, name })
// In a real implementation, this would use the client to submit
return { success: true, requestId: `dsr_${Date.now()}` }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return { success: false, error: message }
}
}
function destroy(): void {
hideBanner()
_client = null
_config = null
_isInitialized = false
log('SDK destroyed')
}
// ============================================================================
// Export for IIFE
// ============================================================================
export const BreakPilotSDK = {
init,
getConsents,
updateConsent,
showBanner,
hideBanner,
hasConsent,
getClient,
submitDSR,
destroy,
version: '0.0.1',
}
// Auto-attach to window for script tag usage
if (typeof window !== 'undefined') {
;(window as unknown as Record<string, unknown>).BreakPilotSDK = BreakPilotSDK
}
export default BreakPilotSDK

View File

@@ -0,0 +1,29 @@
/**
* @breakpilot/compliance-sdk-vanilla
*
* Vanilla JS integration for BreakPilot Compliance SDK
*
* Includes:
* - Embed Script (IIFE for <script> tag)
* - Web Components (Custom Elements)
*/
// Re-export embed script
export { BreakPilotSDK, type BreakPilotSDKConfig, type BannerConfig } from './embed'
// Re-export web components
export {
BreakPilotElement,
COMMON_STYLES,
ConsentBannerElement,
DSRPortalElement,
ComplianceScoreElement,
} from './web-components'
// Re-export types from core
export type {
ConsentPurpose,
CookieBannerPosition,
CookieBannerTheme,
DSRRequestType,
} from '@breakpilot/compliance-sdk-types'

View File

@@ -0,0 +1,95 @@
/**
* Base class for BreakPilot Web Components
*/
import { ComplianceClient } from '@breakpilot/compliance-sdk-core'
export abstract class BreakPilotElement extends HTMLElement {
protected client: ComplianceClient | null = null
protected shadow: ShadowRoot
constructor() {
super()
this.shadow = this.attachShadow({ mode: 'open' })
}
connectedCallback(): void {
this.initializeClient()
this.render()
}
disconnectedCallback(): void {
this.cleanup()
}
attributeChangedCallback(
name: string,
_oldValue: string | null,
_newValue: string | null
): void {
if (name === 'api-key' || name === 'api-endpoint') {
this.initializeClient()
}
this.render()
}
protected initializeClient(): void {
const apiKey = this.getAttribute('api-key')
const apiEndpoint =
this.getAttribute('api-endpoint') || 'https://compliance.breakpilot.app/api/v1'
const tenantId = this.getAttribute('tenant-id') || undefined
if (apiKey) {
this.client = new ComplianceClient({
apiKey,
apiEndpoint,
tenantId,
})
}
}
protected abstract render(): void
protected cleanup(): void {}
protected createStyles(css: string): HTMLStyleElement {
const style = document.createElement('style')
style.textContent = css
return style
}
protected emit<T>(eventName: string, detail: T): void {
this.dispatchEvent(
new CustomEvent(eventName, {
detail,
bubbles: true,
composed: true,
})
)
}
}
// Common styles
export const COMMON_STYLES = `
:host {
display: block;
font-family: system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.5;
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
}
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
}
input, textarea {
font-family: inherit;
font-size: inherit;
}
`

View File

@@ -0,0 +1,136 @@
/**
* <breakpilot-compliance-score> Web Component
*
* Displays a circular compliance score indicator
*
* Usage:
* <breakpilot-compliance-score
* score="75"
* size="medium"
* show-label="true">
* </breakpilot-compliance-score>
*/
import { BreakPilotElement, COMMON_STYLES } from './base'
export class ComplianceScoreElement extends BreakPilotElement {
static get observedAttributes(): string[] {
return ['score', 'size', 'show-label', 'label']
}
private get score(): number {
const value = parseInt(this.getAttribute('score') || '0', 10)
return Math.max(0, Math.min(100, value))
}
private get size(): 'small' | 'medium' | 'large' {
return (this.getAttribute('size') as 'small' | 'medium' | 'large') || 'medium'
}
private get showLabel(): boolean {
return this.getAttribute('show-label') !== 'false'
}
private get label(): string {
return this.getAttribute('label') || 'Compliance Score'
}
private getColor(score: number): string {
if (score >= 80) return '#16a34a' // Green
if (score >= 60) return '#f59e0b' // Yellow
if (score >= 40) return '#f97316' // Orange
return '#dc2626' // Red
}
protected render(): void {
const sizes = {
small: { width: 80, strokeWidth: 6, fontSize: 18 },
medium: { width: 120, strokeWidth: 8, fontSize: 24 },
large: { width: 160, strokeWidth: 10, fontSize: 32 },
}
const { width, strokeWidth, fontSize } = sizes[this.size]
const radius = (width - strokeWidth) / 2
const circumference = radius * 2 * Math.PI
const offset = circumference - (this.score / 100) * circumference
const color = this.getColor(this.score)
const styles = `
${COMMON_STYLES}
:host {
display: inline-flex;
flex-direction: column;
align-items: center;
}
.score-container {
position: relative;
}
svg {
transform: rotate(-90deg);
}
.background-circle {
fill: none;
stroke: #e5e5e5;
}
.progress-circle {
fill: none;
stroke: ${color};
stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease-in-out;
}
.score-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: ${fontSize}px;
font-weight: 700;
color: ${color};
}
.label {
margin-top: 8px;
font-size: 14px;
color: #666;
text-align: center;
}
`
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="score-container">
<svg width="${width}" height="${width}">
<circle
class="background-circle"
cx="${width / 2}"
cy="${width / 2}"
r="${radius}"
stroke-width="${strokeWidth}"
/>
<circle
class="progress-circle"
cx="${width / 2}"
cy="${width / 2}"
r="${radius}"
stroke-width="${strokeWidth}"
stroke-dasharray="${circumference}"
stroke-dashoffset="${offset}"
/>
</svg>
<div class="score-text">${this.score}%</div>
</div>
${this.showLabel ? `<div class="label">${this.label}</div>` : ''}
`
}
}
// Register the custom element
if (typeof customElements !== 'undefined') {
customElements.define('breakpilot-compliance-score', ComplianceScoreElement)
}

View File

@@ -0,0 +1,379 @@
/**
* <breakpilot-consent-banner> Web Component
*
* Usage:
* <breakpilot-consent-banner
* api-key="pk_live_xxx"
* position="bottom"
* theme="light"
* language="de"
* privacy-url="/privacy"
* imprint-url="/imprint">
* </breakpilot-consent-banner>
*/
import type {
ConsentPurpose,
CookieBannerPosition,
CookieBannerTheme,
} from '@breakpilot/compliance-sdk-types'
import { BreakPilotElement, COMMON_STYLES } from './base'
const TRANSLATIONS = {
de: {
title: 'Cookie-Einwilligung',
description:
'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.',
acceptAll: 'Alle akzeptieren',
rejectAll: 'Nur notwendige',
settings: 'Einstellungen',
save: 'Einstellungen speichern',
back: 'Zurück',
privacy: 'Datenschutz',
imprint: 'Impressum',
categories: {
ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' },
FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' },
ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' },
MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' },
},
},
en: {
title: 'Cookie Consent',
description:
'We use cookies to improve your experience. For more information, please see our privacy policy.',
acceptAll: 'Accept All',
rejectAll: 'Reject Non-Essential',
settings: 'Settings',
save: 'Save Settings',
back: 'Back',
privacy: 'Privacy Policy',
imprint: 'Imprint',
categories: {
ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' },
FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' },
ANALYTICS: { name: 'Analytics', description: 'Usage statistics' },
MARKETING: { name: 'Marketing', description: 'Personalized advertising' },
},
},
}
export class ConsentBannerElement extends BreakPilotElement {
static get observedAttributes(): string[] {
return [
'api-key',
'api-endpoint',
'position',
'theme',
'language',
'privacy-url',
'imprint-url',
]
}
private consents: Record<ConsentPurpose, boolean> = {
ESSENTIAL: true,
FUNCTIONAL: false,
ANALYTICS: false,
MARKETING: false,
PERSONALIZATION: false,
THIRD_PARTY: false,
}
private showSettings = false
connectedCallback(): void {
super.connectedCallback()
this.loadStoredConsents()
}
private get position(): CookieBannerPosition {
return (this.getAttribute('position')?.toUpperCase() as CookieBannerPosition) || 'BOTTOM'
}
private get theme(): CookieBannerTheme {
return (this.getAttribute('theme')?.toUpperCase() as CookieBannerTheme) || 'LIGHT'
}
private get language(): 'de' | 'en' {
return (this.getAttribute('language') as 'de' | 'en') || 'de'
}
private get privacyUrl(): string {
return this.getAttribute('privacy-url') || '/privacy'
}
private get imprintUrl(): string {
return this.getAttribute('imprint-url') || '/imprint'
}
private get t() {
return TRANSLATIONS[this.language]
}
private loadStoredConsents(): void {
try {
const stored = localStorage.getItem('breakpilot_consents')
if (stored) {
this.consents = JSON.parse(stored)
this.hide()
}
} catch {
// Ignore errors
}
}
private storeConsents(): void {
try {
localStorage.setItem('breakpilot_consents', JSON.stringify(this.consents))
localStorage.setItem('breakpilot_consents_timestamp', Date.now().toString())
} catch {
// Ignore errors
}
}
private handleAcceptAll = (): void => {
this.consents = {
ESSENTIAL: true,
FUNCTIONAL: true,
ANALYTICS: true,
MARKETING: true,
PERSONALIZATION: true,
THIRD_PARTY: true,
}
this.applyConsents()
}
private handleRejectAll = (): void => {
this.consents = {
ESSENTIAL: true,
FUNCTIONAL: false,
ANALYTICS: false,
MARKETING: false,
PERSONALIZATION: false,
THIRD_PARTY: false,
}
this.applyConsents()
}
private handleSave = (): void => {
this.applyConsents()
}
private applyConsents(): void {
this.storeConsents()
this.emit('consent-change', { consents: this.consents })
this.hide()
}
private toggleSettings = (): void => {
this.showSettings = !this.showSettings
this.render()
}
public show(): void {
this.style.display = 'block'
this.render()
}
public hide(): void {
this.style.display = 'none'
}
public getConsents(): Record<ConsentPurpose, boolean> {
return { ...this.consents }
}
protected render(): void {
const isDark = this.theme === 'DARK'
const bgColor = isDark ? '#1a1a1a' : '#ffffff'
const textColor = isDark ? '#ffffff' : '#1a1a1a'
const borderColor = isDark ? '#333' : '#eee'
const positionStyles = {
TOP: 'top: 0; left: 0; right: 0;',
BOTTOM: 'bottom: 0; left: 0; right: 0;',
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 500px;',
}
const styles = `
${COMMON_STYLES}
:host {
position: fixed;
z-index: 99999;
${positionStyles[this.position]}
}
.banner {
background-color: ${bgColor};
color: ${textColor};
padding: 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.title {
margin: 0 0 10px;
font-size: 18px;
font-weight: 600;
}
.description {
margin: 0 0 15px;
opacity: 0.8;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.btn {
padding: 10px 20px;
border-radius: 4px;
border: none;
font-weight: 500;
}
.btn-primary {
background-color: ${isDark ? '#ffffff' : '#1a1a1a'};
color: ${isDark ? '#1a1a1a' : '#ffffff'};
}
.btn-secondary {
background-color: transparent;
border: 1px solid ${textColor};
color: ${textColor};
}
.links {
margin-left: auto;
font-size: 12px;
}
.links a {
color: ${textColor};
text-decoration: none;
margin-right: 15px;
}
.links a:last-child {
margin-right: 0;
}
.category {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid ${borderColor};
}
.category-name {
font-weight: 500;
}
.category-description {
font-size: 12px;
opacity: 0.7;
}
.checkbox {
width: 20px;
height: 20px;
}
.checkbox:disabled {
cursor: not-allowed;
}
`
if (this.showSettings) {
this.renderSettings(styles)
} else {
this.renderMain(styles)
}
}
private renderMain(styles: string): void {
const t = this.t
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="banner">
<h3 class="title">${t.title}</h3>
<p class="description">${t.description}</p>
<div class="buttons">
<button class="btn btn-primary" id="accept-all">${t.acceptAll}</button>
<button class="btn btn-secondary" id="reject-all">${t.rejectAll}</button>
<button class="btn btn-secondary" id="settings">${t.settings}</button>
<div class="links">
<a href="${this.privacyUrl}">${t.privacy}</a>
<a href="${this.imprintUrl}">${t.imprint}</a>
</div>
</div>
</div>
`
this.shadow.getElementById('accept-all')!.onclick = this.handleAcceptAll
this.shadow.getElementById('reject-all')!.onclick = this.handleRejectAll
this.shadow.getElementById('settings')!.onclick = this.toggleSettings
}
private renderSettings(styles: string): void {
const t = this.t
const categories: ConsentPurpose[] = ['ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING']
const categoriesHtml = categories
.map(
cat => `
<div class="category">
<div>
<div class="category-name">${t.categories[cat].name}</div>
<div class="category-description">${t.categories[cat].description}</div>
</div>
<input
type="checkbox"
class="checkbox"
data-category="${cat}"
${this.consents[cat] ? 'checked' : ''}
${cat === 'ESSENTIAL' ? 'disabled' : ''}
/>
</div>
`
)
.join('')
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="banner">
<h3 class="title">${t.settings}</h3>
${categoriesHtml}
<div class="buttons" style="margin-top: 15px;">
<button class="btn btn-primary" id="save">${t.save}</button>
<button class="btn btn-secondary" id="back">${t.back}</button>
</div>
</div>
`
this.shadow.getElementById('save')!.onclick = this.handleSave
this.shadow.getElementById('back')!.onclick = this.toggleSettings
// Handle checkbox changes
this.shadow.querySelectorAll<HTMLInputElement>('.checkbox').forEach(checkbox => {
checkbox.onchange = () => {
const category = checkbox.dataset.category as ConsentPurpose
if (category !== 'ESSENTIAL') {
this.consents[category] = checkbox.checked
}
}
})
}
}
// Register the custom element
if (typeof customElements !== 'undefined') {
customElements.define('breakpilot-consent-banner', ConsentBannerElement)
}

View File

@@ -0,0 +1,464 @@
/**
* <breakpilot-dsr-portal> Web Component
*
* Data Subject Request Portal for GDPR rights
*
* Usage:
* <breakpilot-dsr-portal
* api-key="pk_live_xxx"
* language="de">
* </breakpilot-dsr-portal>
*/
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
import { BreakPilotElement, COMMON_STYLES } from './base'
const TRANSLATIONS = {
de: {
title: 'Betroffenenrechte-Portal',
subtitle:
'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.',
requestType: 'Art der Anfrage',
name: 'Ihr Name',
namePlaceholder: 'Max Mustermann',
email: 'E-Mail-Adresse',
emailPlaceholder: 'max@example.com',
additionalInfo: 'Zusätzliche Informationen (optional)',
additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...',
submit: 'Anfrage einreichen',
submitting: 'Wird gesendet...',
successTitle: 'Anfrage eingereicht',
successMessage:
'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an',
disclaimer:
'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.',
types: {
ACCESS: {
name: 'Auskunft (Art. 15)',
description: 'Welche Daten haben Sie über mich gespeichert?',
},
RECTIFICATION: {
name: 'Berichtigung (Art. 16)',
description: 'Korrigieren Sie falsche Daten über mich.',
},
ERASURE: {
name: 'Löschung (Art. 17)',
description: 'Löschen Sie alle meine personenbezogenen Daten.',
},
PORTABILITY: {
name: 'Datenübertragbarkeit (Art. 20)',
description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.',
},
RESTRICTION: {
name: 'Einschränkung (Art. 18)',
description: 'Schränken Sie die Verarbeitung meiner Daten ein.',
},
OBJECTION: {
name: 'Widerspruch (Art. 21)',
description: 'Ich widerspreche der Verarbeitung meiner Daten.',
},
},
},
en: {
title: 'Data Subject Rights Portal',
subtitle:
'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.',
requestType: 'Request Type',
name: 'Your Name',
namePlaceholder: 'John Doe',
email: 'Email Address',
emailPlaceholder: 'john@example.com',
additionalInfo: 'Additional Information (optional)',
additionalInfoPlaceholder: 'Any additional details about your request...',
submit: 'Submit Request',
submitting: 'Submitting...',
successTitle: 'Request Submitted',
successMessage:
'We will process your request within 30 days. You will receive a confirmation email at',
disclaimer:
'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.',
types: {
ACCESS: {
name: 'Access (Art. 15)',
description: 'What data do you have about me?',
},
RECTIFICATION: {
name: 'Rectification (Art. 16)',
description: 'Correct inaccurate data about me.',
},
ERASURE: {
name: 'Erasure (Art. 17)',
description: 'Delete all my personal data.',
},
PORTABILITY: {
name: 'Data Portability (Art. 20)',
description: 'Export my data in a machine-readable format.',
},
RESTRICTION: {
name: 'Restriction (Art. 18)',
description: 'Restrict the processing of my data.',
},
OBJECTION: {
name: 'Objection (Art. 21)',
description: 'I object to the processing of my data.',
},
},
},
}
export class DSRPortalElement extends BreakPilotElement {
static get observedAttributes(): string[] {
return ['api-key', 'api-endpoint', 'tenant-id', 'language']
}
private selectedType: DSRRequestType | null = null
private name = ''
private email = ''
private additionalInfo = ''
private isSubmitting = false
private isSubmitted = false
private error: string | null = null
private get language(): 'de' | 'en' {
return (this.getAttribute('language') as 'de' | 'en') || 'de'
}
private get t() {
return TRANSLATIONS[this.language]
}
private handleTypeSelect = (type: DSRRequestType): void => {
this.selectedType = type
this.render()
}
private handleSubmit = async (e: Event): Promise<void> => {
e.preventDefault()
if (!this.selectedType || !this.email || !this.name) {
return
}
this.isSubmitting = true
this.error = null
this.render()
try {
// In a real implementation, this would use the client
// For now, we simulate a successful submission
await new Promise(resolve => setTimeout(resolve, 1000))
this.emit('dsr-submitted', {
type: this.selectedType,
email: this.email,
name: this.name,
additionalInfo: this.additionalInfo,
})
this.isSubmitted = true
} catch (err) {
this.error = err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten'
} finally {
this.isSubmitting = false
this.render()
}
}
protected render(): void {
const styles = `
${COMMON_STYLES}
:host {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.portal {
background: #fff;
}
.title {
margin: 0 0 10px;
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
}
.subtitle {
margin: 0 0 20px;
color: #666;
}
.form-group {
margin-bottom: 20px;
}
.label {
display: block;
font-weight: 500;
margin-bottom: 10px;
}
.type-options {
display: grid;
gap: 10px;
}
.type-option {
display: flex;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
background: #fff;
transition: all 0.2s;
}
.type-option:hover {
border-color: #999;
}
.type-option.selected {
border-color: #1a1a1a;
background: #f5f5f5;
}
.type-option input {
margin-right: 15px;
}
.type-name {
font-weight: 500;
}
.type-description {
font-size: 13px;
color: #666;
}
.input {
width: 100%;
padding: 12px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.input:focus {
outline: none;
border-color: #1a1a1a;
}
.textarea {
resize: vertical;
min-height: 100px;
}
.error {
padding: 12px;
background: #fef2f2;
color: #dc2626;
border-radius: 4px;
margin-bottom: 15px;
}
.btn-submit {
width: 100%;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
color: #fff;
background: #1a1a1a;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.disclaimer {
margin-top: 20px;
font-size: 12px;
color: #666;
}
.success {
text-align: center;
padding: 40px 20px;
background: #f0fdf4;
border-radius: 8px;
}
.success-icon {
font-size: 48px;
margin-bottom: 20px;
}
.success-title {
margin: 0 0 10px;
color: #166534;
}
.success-message {
margin: 0;
color: #166534;
}
`
if (this.isSubmitted) {
this.renderSuccess(styles)
} else {
this.renderForm(styles)
}
}
private renderForm(styles: string): void {
const t = this.t
const types: DSRRequestType[] = [
'ACCESS',
'RECTIFICATION',
'ERASURE',
'PORTABILITY',
'RESTRICTION',
'OBJECTION',
]
const typesHtml = types
.map(
type => `
<label class="type-option ${this.selectedType === type ? 'selected' : ''}">
<input
type="radio"
name="dsrType"
value="${type}"
${this.selectedType === type ? 'checked' : ''}
/>
<div>
<div class="type-name">${t.types[type].name}</div>
<div class="type-description">${t.types[type].description}</div>
</div>
</label>
`
)
.join('')
const isValid = this.selectedType && this.email && this.name
const isDisabled = !isValid || this.isSubmitting
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="portal">
<h2 class="title">${t.title}</h2>
<p class="subtitle">${t.subtitle}</p>
<form id="dsr-form">
<div class="form-group">
<label class="label">${t.requestType} *</label>
<div class="type-options">
${typesHtml}
</div>
</div>
<div class="form-group">
<label class="label">${t.name} *</label>
<input
type="text"
class="input"
id="name-input"
value="${this.name}"
placeholder="${t.namePlaceholder}"
required
/>
</div>
<div class="form-group">
<label class="label">${t.email} *</label>
<input
type="email"
class="input"
id="email-input"
value="${this.email}"
placeholder="${t.emailPlaceholder}"
required
/>
</div>
<div class="form-group">
<label class="label">${t.additionalInfo}</label>
<textarea
class="input textarea"
id="info-input"
placeholder="${t.additionalInfoPlaceholder}"
rows="4"
>${this.additionalInfo}</textarea>
</div>
${this.error ? `<div class="error">${this.error}</div>` : ''}
<button
type="submit"
class="btn-submit"
${isDisabled ? 'disabled' : ''}
>
${this.isSubmitting ? t.submitting : t.submit}
</button>
</form>
<p class="disclaimer">${t.disclaimer}</p>
</div>
`
// Bind events
const form = this.shadow.getElementById('dsr-form') as HTMLFormElement
form.onsubmit = this.handleSubmit
const nameInput = this.shadow.getElementById('name-input') as HTMLInputElement
nameInput.oninput = e => {
this.name = (e.target as HTMLInputElement).value
}
const emailInput = this.shadow.getElementById('email-input') as HTMLInputElement
emailInput.oninput = e => {
this.email = (e.target as HTMLInputElement).value
}
const infoInput = this.shadow.getElementById('info-input') as HTMLTextAreaElement
infoInput.oninput = e => {
this.additionalInfo = (e.target as HTMLTextAreaElement).value
}
// Bind radio buttons
this.shadow.querySelectorAll<HTMLInputElement>('input[name="dsrType"]').forEach(radio => {
radio.onchange = () => {
this.handleTypeSelect(radio.value as DSRRequestType)
}
})
}
private renderSuccess(styles: string): void {
const t = this.t
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="portal">
<div class="success">
<div class="success-icon">✓</div>
<h2 class="success-title">${t.successTitle}</h2>
<p class="success-message">
${t.successMessage} ${this.email}.
</p>
</div>
</div>
`
}
}
// Register the custom element
if (typeof customElements !== 'undefined') {
customElements.define('breakpilot-dsr-portal', DSRPortalElement)
}

View File

@@ -0,0 +1,13 @@
/**
* BreakPilot Compliance SDK - Web Components
*
* Available components:
* - <breakpilot-consent-banner>
* - <breakpilot-dsr-portal>
* - <breakpilot-compliance-score>
*/
export { BreakPilotElement, COMMON_STYLES } from './base'
export { ConsentBannerElement } from './consent-banner'
export { DSRPortalElement } from './dsr-portal'
export { ComplianceScoreElement } from './compliance-score'