diff --git a/admin/views/settings-general.php b/admin/views/settings-general.php
index 04a1cb2..abcc442 100644
--- a/admin/views/settings-general.php
+++ b/admin/views/settings-general.php
@@ -101,6 +101,42 @@ if ( ! defined( 'ABSPATH' ) ) {
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
|
diff --git a/includes/class-admin.php b/includes/class-admin.php
index 553c033..7d605bc 100644
--- a/includes/class-admin.php
+++ b/includes/class-admin.php
@@ -164,6 +164,12 @@ class ATT_Consent_Admin {
$settings['consent_mode'] = in_array( $_POST['consent_mode'] ?? '', array( 'advanced', 'basic' ), true ) ? $_POST['consent_mode'] : 'advanced';
$settings['banner_position'] = in_array( $_POST['banner_position'] ?? '', array( 'bottom', 'top', 'center' ), true ) ? $_POST['banner_position'] : 'bottom';
$settings['consent_expiry'] = absint( $_POST['consent_expiry'] ?? 365 );
+ $settings['floating_widget'] = in_array(
+ $_POST['floating_widget'] ?? '',
+ array( 'bottom-right', 'right', 'none' ),
+ true
+ ) ? $_POST['floating_widget'] : 'bottom-right';
+ $settings['floating_widget_label'] = sanitize_text_field( $_POST['floating_widget_label'] ?? '' );
break;
case 'appearance':
diff --git a/includes/class-att-consent.php b/includes/class-att-consent.php
index d4da47d..519884a 100644
--- a/includes/class-att-consent.php
+++ b/includes/class-att-consent.php
@@ -29,8 +29,10 @@ class ATT_Consent {
'ga4_measurement_id' => '',
'gtm_container_id' => '',
'consent_mode' => 'advanced',
- 'banner_position' => 'bottom',
- 'consent_expiry' => 365,
+ 'banner_position' => 'bottom',
+ 'consent_expiry' => 365,
+ 'floating_widget' => 'bottom-right',
+ 'floating_widget_label' => 'Cookie Settings',
// Appearance.
'banner_bg_color' => '#1a1a2e',
diff --git a/includes/class-frontend.php b/includes/class-frontend.php
index 7f4a76a..fb0ee83 100644
--- a/includes/class-frontend.php
+++ b/includes/class-frontend.php
@@ -284,7 +284,14 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
';
- $html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html, $s );
+ $widget_html = '';
+ if ( 'none' !== $s['floating_widget'] ) {
+ $widget_position = 'right' === $s['floating_widget'] ? 'att-cc-widget--right' : 'att-cc-widget--bottom-right';
+ $widget_label = ! empty( $s['floating_widget_label'] ) ? $s['floating_widget_label'] : 'Cookie Settings';
+ $widget_html = '';
+ }
+
+ $html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html . $widget_html, $s );
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
do_action( 'att_consent_after_banner' );
diff --git a/public/css/banner.css b/public/css/banner.css
index a5e0f11..af3fbac 100644
--- a/public/css/banner.css
+++ b/public/css/banner.css
@@ -248,6 +248,55 @@ body.att-cc-modal-active {
overflow: hidden;
}
+/* --- Floating Widget --- */
+
+.att-cc-widget {
+ position: fixed;
+ z-index: 999998;
+ padding: 8px 14px;
+ border: none;
+ border-radius: 20px;
+ cursor: pointer;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
+ font-size: 12px;
+ font-weight: 600;
+ background: var(--att-cc-bg, #1a1a2e);
+ color: var(--att-cc-text, #fff);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ transition: opacity 0.2s;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.att-cc-widget:hover {
+ opacity: 0.9;
+}
+
+.att-cc-widget:focus-visible {
+ outline: 2px solid #fff;
+ outline-offset: 2px;
+}
+
+.att-cc-widget--bottom-right {
+ bottom: 16px;
+ right: 16px;
+}
+
+.att-cc-widget--right {
+ right: 0;
+ top: 50%;
+ transform: rotate(-90deg) translateX(50%);
+ transform-origin: right center;
+ border-radius: 8px 8px 0 0;
+}
+
+.att-cc-widget svg {
+ width: 16px;
+ height: 16px;
+ fill: currentColor;
+}
+
/* --- Responsive --- */
@media (max-width: 600px) {
diff --git a/public/js/banner.js b/public/js/banner.js
index 38e797f..5ef858a 100644
--- a/public/js/banner.js
+++ b/public/js/banner.js
@@ -1,7 +1,8 @@
/**
* ATT Consent Banner UI
*
- * Handles banner display, preferences modal, focus trapping, and user interactions.
+ * Handles banner display, preferences modal, floating widget, anchor link
+ * triggers, focus trapping, and user interactions.
* Depends on AttConsent (consent-manager.js) for consent state management.
*/
(function() {
@@ -9,43 +10,12 @@
var banner = document.getElementById('att-cc-banner');
var modal = document.getElementById('att-cc-modal');
+ var widget = document.getElementById('att-cc-widget');
if (!banner || !modal) {
return;
}
- // If consent already exists, hide the banner immediately.
- if (AttConsent.hasConsent()) {
- hideBanner();
- return;
- }
-
- // --- Banner buttons ---
-
- var acceptBtn = banner.querySelector('[data-att-cc="accept-all"]');
- var rejectBtn = banner.querySelector('[data-att-cc="reject-all"]');
- var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
-
- if (acceptBtn) {
- acceptBtn.addEventListener('click', function() {
- AttConsent.update({ functional: true, analytics: true, marketing: true });
- hideBanner();
- });
- }
-
- if (rejectBtn) {
- rejectBtn.addEventListener('click', function() {
- AttConsent.update({ functional: false, analytics: false, marketing: false });
- hideBanner();
- });
- }
-
- if (prefsBtn) {
- prefsBtn.addEventListener('click', function() {
- openModal();
- });
- }
-
// --- Modal buttons ---
var saveBtn = modal.querySelector('[data-att-cc="save-preferences"]');
@@ -62,6 +32,7 @@
AttConsent.update(categories);
closeModal();
hideBanner();
+ showWidget();
});
}
@@ -77,6 +48,59 @@
});
}
+ // --- Floating widget click handler ---
+
+ if (widget) {
+ widget.addEventListener('click', function() {
+ openModal();
+ });
+ }
+
+ // --- Anchor link listener ---
+
+ document.addEventListener('click', function(e) {
+ var link = e.target.closest('a[href="#att-cc-preferences"]');
+ if (link) {
+ e.preventDefault();
+ openModal();
+ }
+ });
+
+ // --- Initial state ---
+
+ if (AttConsent.hasConsent()) {
+ hideBanner();
+ showWidget();
+ } else {
+ // --- Banner buttons (only needed for first-time visitors) ---
+
+ var acceptBtn = banner.querySelector('[data-att-cc="accept-all"]');
+ var rejectBtn = banner.querySelector('[data-att-cc="reject-all"]');
+ var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
+
+ if (acceptBtn) {
+ acceptBtn.addEventListener('click', function() {
+ AttConsent.update({ functional: true, analytics: true, marketing: true });
+ hideBanner();
+ showWidget();
+ });
+ }
+
+ if (rejectBtn) {
+ rejectBtn.addEventListener('click', function() {
+ AttConsent.update({ functional: false, analytics: false, marketing: false });
+ hideBanner();
+ showWidget();
+ });
+ }
+
+ if (prefsBtn) {
+ prefsBtn.addEventListener('click', function() {
+ openModal();
+ });
+ }
+ }
+
// --- Helper functions ---
function hideBanner() {
@@ -84,6 +108,12 @@
banner.classList.add('att-cc-hidden');
}
+ function showWidget() {
+ if (widget) {
+ widget.style.display = '';
+ }
+ }
+
function openModal() {
// Pre-fill checkboxes with existing consent if available.
var existing = AttConsent.getConsent();
@@ -104,9 +134,14 @@
modal.classList.remove('att-cc-modal--open');
document.body.classList.remove('att-cc-modal-active');
- // Return focus to the preferences button.
- if (prefsBtn) {
- prefsBtn.focus();
+ // Return focus to the widget if visible, otherwise the banner prefs button.
+ if (widget && widget.style.display !== 'none') {
+ widget.focus();
+ } else {
+ var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
+ if (prefsBtn) {
+ prefsBtn.focus();
+ }
}
}
@@ -162,13 +197,4 @@
element.addEventListener('keydown', handleKeydown);
}
-
- // Listen for programmatic consent revocation to re-show banner.
- document.addEventListener('att_consent_update', function(e) {
- var cats = e.detail;
- if (!cats.functional && !cats.analytics && !cats.marketing) {
- // All denied - consent was effectively revoked, but don't re-show
- // banner immediately (user just clicked reject).
- }
- });
})();
|