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,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'