/** * Cookie banner DOM rendering for the vanilla embed SDK. * * Phase 4: extracted from embed.ts. The banner module is pure rendering — * caller passes in a BannerContext describing current consent state plus * callbacks for user actions. */ import type { ConsentPurpose, CookieBannerPosition, CookieBannerTheme, } from '@breakpilot/compliance-sdk-types' import { TRANSLATIONS, createElement, type BannerLanguage, } from './embed-translations' export interface BannerConfig { position?: CookieBannerPosition theme?: CookieBannerTheme language?: BannerLanguage 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 interface BannerCallbacks { onAcceptAll: () => void onRejectAll: () => void onShowSettings: () => void onToggleCategory: (category: ConsentPurpose, granted: boolean) => void onSaveSettings: () => void onBack: () => void } export interface BannerContext { config: BannerConfig consents: Record callbacks: BannerCallbacks } interface ResolvedColors { bgColor: string textColor: string primaryColor: string secondaryColor: string isDark: boolean } function resolveColors(config: BannerConfig): ResolvedColors { const theme = config.theme || 'LIGHT' const isDark = theme === 'DARK' return { isDark, bgColor: config.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff'), textColor: config.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a'), primaryColor: config.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a'), secondaryColor: config.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a'), } } export function createBannerElement(ctx: BannerContext): HTMLElement { const { config, callbacks } = ctx const lang: BannerLanguage = config.language || 'de' const t = TRANSLATIONS[lang] const position = config.position || 'BOTTOM' const colors = resolveColors(config) const { bgColor, textColor, primaryColor, secondaryColor, isDark } = colors 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 } ) const title = createElement('h3', { margin: '0 0 10px', fontSize: '18px', fontWeight: '600', }) title.textContent = config.texts?.title || t.title const description = createElement('p', { margin: '0 0 15px', opacity: '0.8', }) description.textContent = config.texts?.description || t.description const buttonsContainer = createElement('div', { display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center', }) 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 = config.texts?.acceptAll || t.acceptAll acceptBtn.onclick = () => callbacks.onAcceptAll() 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 = config.texts?.rejectAll || t.rejectAll rejectBtn.onclick = () => callbacks.onRejectAll() 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 = config.texts?.settings || t.settings settingsBtn.onclick = () => callbacks.onShowSettings() const linksContainer = createElement('div', { marginLeft: 'auto', fontSize: '12px', }) const privacyLink = createElement('a', { marginRight: '15px', color: textColor, textDecoration: 'none', }) privacyLink.href = config.privacyPolicyUrl || '/privacy' privacyLink.textContent = t.privacy const imprintLink = createElement('a', { color: textColor, textDecoration: 'none', }) imprintLink.href = config.imprintUrl || '/imprint' imprintLink.textContent = t.imprint 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 } export function renderSettingsPanel(element: HTMLElement, ctx: BannerContext): void { const { config, consents, callbacks } = ctx const lang: BannerLanguage = config.language || 'de' const t = TRANSLATIONS[lang] const colors = resolveColors(config) const { textColor, primaryColor, secondaryColor, isDark } = colors element.innerHTML = '' const title = createElement('h3', { margin: '0 0 15px', fontSize: '18px', fontWeight: '600', }) title.textContent = t.settings element.appendChild(title) 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') { callbacks.onToggleCategory(category, checkbox.checked) } } row.appendChild(labelContainer) row.appendChild(checkbox) element.appendChild(row) }) const buttonsContainer = createElement('div', { display: 'flex', flexWrap: 'wrap', gap: '10px', marginTop: '15px', }) 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 = config.texts?.save || t.save saveBtn.onclick = () => callbacks.onSaveSettings() 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 = () => callbacks.onBack() buttonsContainer.appendChild(saveBtn) buttonsContainer.appendChild(backBtn) element.appendChild(buttonsContainer) }