import * as shared from './shared.js'; import { LitElement, html, css, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; import { property } from 'lit/decorators/property.js'; import * as csInterfaces from '@consent.software/interfaces'; import * as csWebclient from '@consent.software/webclient'; import { delayFor } from '@push.rocks/smartdelay'; declare global { interface HTMLElementTagNameMap { 'consentsoftware-cookieconsent': ConsentsoftwareCookieconsent; } } @customElement('consentsoftware-cookieconsent') export class ConsentsoftwareCookieconsent extends LitElement { public static demo = () => html``; public csWebclientInstance = new csWebclient.CsWebclient(); public csWebclientRan = false; // Reflects the current theme ('light' or 'dark') @property({ type: String, reflect: true }) public accessor theme: 'light' | 'dark' = 'light'; /** * Define component styles with CSS variables that adjust based on theme. * The default variables serve as baseline for the light theme. * Theme-specific overrides modify these for dark mode. */ public static styles = css` :host { font-family: ${shared.fontStack}; font-size: 14px; user-select: none; /* Shadcn-inspired Dark Theme (default) */ --background: hsl(0 0% 7%); --foreground: hsl(0 0% 95%); --muted: hsl(0 0% 15%); --muted-foreground: hsl(0 0% 64%); --border: hsl(0 0% 18%); --input: hsl(0 0% 15%); --primary: hsl(0 0% 98%); --primary-foreground: hsl(0 0% 9%); --secondary: hsl(0 0% 15%); --secondary-foreground: hsl(0 0% 98%); --accent: hsl(0 0% 15%); --accent-foreground: hsl(0 0% 98%); --ring: hsl(0 0% 30%); --radius: 8px; } :host([theme='dark']) { --background: hsl(0 0% 7%); --foreground: hsl(0 0% 95%); --muted: hsl(0 0% 15%); --muted-foreground: hsl(0 0% 64%); --border: hsl(0 0% 18%); --input: hsl(0 0% 15%); --primary: hsl(0 0% 98%); --primary-foreground: hsl(0 0% 9%); --secondary: hsl(0 0% 15%); --secondary-foreground: hsl(0 0% 98%); --accent: hsl(0 0% 15%); --accent-foreground: hsl(0 0% 98%); --ring: hsl(0 0% 30%); } :host([theme='light']) { --background: hsl(0 0% 100%); --foreground: hsl(0 0% 9%); --muted: hsl(0 0% 96%); --muted-foreground: hsl(0 0% 45%); --border: hsl(0 0% 90%); --input: hsl(0 0% 90%); --primary: hsl(0 0% 9%); --primary-foreground: hsl(0 0% 98%); --secondary: hsl(0 0% 96%); --secondary-foreground: hsl(0 0% 9%); --accent: hsl(0 0% 96%); --accent-foreground: hsl(0 0% 9%); --ring: hsl(0 0% 64%); } .pageOverlay { position: fixed; inset: 0; display: grid; align-items: center; justify-content: center; z-index: 1000; background: rgba(0, 0, 0, 0); backdrop-filter: blur(0px); transition: all 0.2s ease-out; } .pageOverlay.shake { background: rgba(0, 0, 0, 0.6) !important; } .modalBox { display: block; color: var(--foreground); background: var(--background); box-shadow: 0 0 0 1px var(--border), 0 16px 70px rgba(0, 0, 0, 0.35); position: relative; border-radius: var(--radius); max-width: 520px; min-width: 320px; box-sizing: border-box; overflow: hidden; will-change: transform; transition: all 0.2s ease-out; transform: scale(0.96); opacity: 0; } @media (max-width: 560px) { .modalBox { max-width: 100%; min-width: 100%; height: 100vh; border-radius: 0; } } .modalBox.shake { animation: shake 120ms 2 linear; } @keyframes shake { 0% { transform: translateX(3px); } 50% { transform: translateX(-3px); } 100% { transform: translateX(0); } } :host([show='false']) { display: none; } :host([show='true']) { display: block; } .content { margin: auto; } .text-container { padding: 12px 16px; font-size: 0.9em; line-height: 1.5; color: var(--muted-foreground); } .text-container a { color: var(--foreground); text-decoration: underline; text-underline-offset: 2px; } .text-container a:hover { opacity: 0.8; } .button-container { padding: 12px 16px 16px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } @media (max-width: 560px) { .button-container { grid-template-columns: 1fr; } } .consent-button { border-radius: calc(var(--radius) - 2px); background: var(--secondary); border: 1px solid var(--border); padding: 8px 16px; font-size: 0.85em; font-weight: 500; text-align: center; cursor: pointer; transition: all 0.15s ease; color: var(--secondary-foreground); } .consent-button:hover { background: var(--accent); border-color: var(--ring); } .consent-button:last-child { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); } .consent-button:last-child:hover { opacity: 0.9; } .info-container { text-align: center; padding: 10px 16px; background: var(--muted); border-top: 1px solid var(--border); font-size: 0.75em; color: var(--muted-foreground); } .info-container a { color: var(--foreground); text-decoration: none; } .info-container a:hover { text-decoration: underline; } `; constructor() { super(); // Initially hide the consent banner until needed this.setAttribute('show', 'false'); } public render(): TemplateResult { return html`
`; } /** * Lifecycle method called when the element is connected to the DOM. * It sets up the theme and displays the consent banner if no cookie levels are set. */ public async connectedCallback() { super.connectedCallback(); this.updateTheme(); // Initialize theme based on system preference const cookieLevel = await this.csWebclientInstance.getCookieLevels(); if (!cookieLevel) { // Show consent banner if cookie levels haven't been set yet this.setAttribute('show', 'true'); requestAnimationFrame(async () => { await this.updated(); const pageOverlay: HTMLDivElement = this.shadowRoot?.querySelector('.pageOverlay'); if (pageOverlay) { // Apply subtle backdrop blur when modal appears pageOverlay.style.background = 'rgba(0,0,0, 0.6)'; pageOverlay.style.backdropFilter = 'blur(4px)'; } const modalBox: HTMLDivElement = this.shadowRoot?.querySelector('.modalBox'); if (modalBox) { // Animate modal box appearance modalBox.style.transform = `scale(1)`; modalBox.style.opacity = '1'; } }); } else { // Hide banner if cookie levels are already set this.setAttribute('show', 'false'); } } public async firstUpdated() { // Placeholder for any logic needed after first render } /** * Called after updates. Logs banner height and runs consent scripts if necessary. */ public async updated() { console.log(`The height of the cookie banner is ${this.shadowRoot?.host?.clientHeight}px`); const acceptedCookieLevels = await this.csWebclientInstance.getCookieLevels(); if (!this.csWebclientRan && acceptedCookieLevels) { this.csWebclientRan = true; await this.csWebclientInstance.getAndRunConsentTuples(); } } /** * Handles consent button clicks, sets cookie levels, and hides the banner. * Uses theme variables for styling transitions. */ private async handleConsentButtonClick( event: MouseEvent, levelsArg: csInterfaces.TCookieLevel[] ) { console.log(`Set level to ${levelsArg}`); const pageOverlay: HTMLDivElement = this.shadowRoot?.querySelector('.pageOverlay'); if (pageOverlay) { pageOverlay.style.background = 'rgba(0,0,0, 0)'; pageOverlay.style.backdropFilter = 'blur(0px)'; } const modalBox: HTMLDivElement = this.shadowRoot?.querySelector('.modalBox'); if (modalBox) { // Scale down and fade out modal box before hiding modalBox.style.transform = `scale(0.95)`; modalBox.style.opacity = '0'; } // Save user consent preferences await this.csWebclientInstance.setCookieLevels(levelsArg); await delayFor(300); this.setAttribute('show', 'false'); // Hide the consent banner this.updated(); // Trigger any post-consent actions } /** * Handles clicks on the page overlay. If clicked outside the modal, * triggers a shake animation as feedback. */ private async pageOverlayClick(e: MouseEvent) { if (e.target === e.currentTarget) { const pageOverlay: HTMLDivElement = this.shadowRoot?.querySelector('.pageOverlay'); const modalBox: HTMLDivElement = this.shadowRoot?.querySelector('.modalBox'); if (pageOverlay && modalBox) { pageOverlay.classList.add('shake'); modalBox.classList.add('shake'); await delayFor(2000); pageOverlay.classList.remove('shake'); modalBox.classList.remove('shake'); } } } /** * Dynamically switches the theme between light and dark. * Listens for system theme changes to update the component's theme. */ private updateTheme() { // Check the initial system preference for dark mode const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; this.theme = prefersDark ? 'dark' : 'light'; // Listen for changes in the system color scheme preference window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { this.theme = e.matches ? 'dark' : 'light'; }); } }