refactor(compliance-sdk): split client/provider/embed/state under 500 LOC
Phase 4 continuation. All touched files now under the file-size cap, and drive-by fixes unblock the types/core/react/vanilla builds which were broken at baseline. Splits - packages/types/src/state 505 -> 31 LOC barrel + state-flow/-assessment/-core - packages/core/src/client 521 -> 395 LOC + client-http 187 LOC (HTTP transport) - packages/react/src/provider 539 -> 460 LOC + provider-context 101 LOC - packages/vanilla/src/embed 611 -> 290 LOC + embed-banner 321 + embed-translations 78 Drive-by fixes (pre-existing typecheck/build failures) - types/rag.ts: rename colliding LegalDocument export to RagLegalDocument (the `export *` chain in index.ts was ambiguous; two consumers updated - core/modules/rag.ts drops unused import, vue/composables/useRAG.ts switches to the renamed symbol). - core/modules/rag.ts: wrap client searchRAG response to add the missing `query` field so the declared SearchResponse return type is satisfied. - react/provider.tsx: re-export useCompliance so ComplianceDashboard / ConsentBanner / DSRPortal legacy `from '../provider'` imports resolve. - vanilla/embed.ts + web-components/base.ts: default tenantId to '' so ComplianceClient construction typechecks. - vanilla/web-components/consent-banner.ts: tighten categories literal to `as const` so t.categories indexing narrows correctly. Verification: packages/types + core + react + vanilla all `pnpm build` clean with DTS emission. consent-sdk unaffected (still green). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,14 @@
|
||||
import { ComplianceClient } from '@breakpilot/compliance-sdk-core'
|
||||
import type {
|
||||
ConsentPurpose,
|
||||
CookieBannerPosition,
|
||||
CookieBannerTheme,
|
||||
DSRRequestType,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
createBannerElement,
|
||||
renderSettingsPanel,
|
||||
type BannerConfig,
|
||||
type BannerContext,
|
||||
} from './embed-banner'
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -37,27 +41,7 @@ export interface BreakPilotSDKConfig {
|
||||
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
|
||||
}
|
||||
}
|
||||
export type { BannerConfig } from './embed-banner'
|
||||
|
||||
// ============================================================================
|
||||
// Internal State
|
||||
@@ -76,51 +60,6 @@ let _consents: Record<ConsentPurpose, boolean> = {
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -149,301 +88,40 @@ function storeConsents(consents: Record<ConsentPurpose, boolean>): void {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// Banner Orchestration
|
||||
// ============================================================================
|
||||
|
||||
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',
|
||||
function buildBannerContext(): BannerContext {
|
||||
return {
|
||||
config: _config?.bannerConfig || {},
|
||||
consents: _consents,
|
||||
callbacks: {
|
||||
onAcceptAll: () => handleAcceptAll(),
|
||||
onRejectAll: () => handleRejectAll(),
|
||||
onShowSettings: () => showSettingsPanel(),
|
||||
onToggleCategory: (category, granted) => {
|
||||
_consents[category] = granted
|
||||
},
|
||||
{
|
||||
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',
|
||||
onSaveSettings: () => handleSaveSettings(),
|
||||
onBack: () => showMainBanner(),
|
||||
},
|
||||
{ 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()
|
||||
_bannerElement = createBannerElement(buildBannerContext())
|
||||
document.body.appendChild(_bannerElement)
|
||||
}
|
||||
|
||||
function showSettingsPanel(): void {
|
||||
if (!_bannerElement) return
|
||||
renderSettingsPanel(_bannerElement, buildBannerContext())
|
||||
}
|
||||
|
||||
function hideBanner(): void {
|
||||
if (_bannerElement) {
|
||||
_bannerElement.remove()
|
||||
@@ -507,7 +185,7 @@ function init(config: BreakPilotSDKConfig): void {
|
||||
_client = new ComplianceClient({
|
||||
apiEndpoint: config.apiEndpoint,
|
||||
apiKey: config.apiKey,
|
||||
tenantId: config.tenantId,
|
||||
tenantId: config.tenantId ?? '',
|
||||
})
|
||||
|
||||
// Check for stored consents
|
||||
|
||||
Reference in New Issue
Block a user