From f3438d6791b09bb748d2293f325ed6cf5380ffa4 Mon Sep 17 00:00:00 2001 From: Alfredo Toselli Date: Wed, 21 Oct 2026 18:32:59 +0200 Subject: [PATCH 1/3] feat: cookie modal --- .claude/launch.json | 11 + source/_static/cookie-settings.css | 212 ++++++++++++++++++ source/_static/matomo.js | 111 ++++++++- .../partials/cookie-settings-modal.html | 61 +++++ source/_templates/partials/site-foot.html | 1 + source/conf.py | 14 +- 6 files changed, 394 insertions(+), 16 deletions(-) create mode 100644 .claude/launch.json create mode 100644 source/_static/cookie-settings.css create mode 100644 source/_templates/partials/cookie-settings-modal.html diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..37800e7 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "docs", + "runtimeExecutable": "make", + "runtimeArgs": ["serve"], + "port": 8000 + } + ] +} diff --git a/source/_static/cookie-settings.css b/source/_static/cookie-settings.css new file mode 100644 index 0000000..31e05df --- /dev/null +++ b/source/_static/cookie-settings.css @@ -0,0 +1,212 @@ +/* + Copyright 2026 European Union + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* Matches the webportal's CookieSettingsModal (HeroUI Modal, size 4xl). */ +.edh-cookie-settings { + --edh-cookie-settings-text: #1d3c7c; + --edh-cookie-settings-violet: #8739c4; + --edh-cookie-settings-switch: #7828c8; + --edh-cookie-settings-switch-off: #d4d4d8; + --edh-cookie-settings-danger: #f31260; + --edh-cookie-settings-focus: #006fee; + + background: #fff; + border-radius: 14px; + box-shadow: + 0 0 5px 0 rgb(0 0 0 / 2%), + 0 2px 10px 0 rgb(0 0 0 / 6%), + 0 0 1px 0 rgb(0 0 0 / 30%); + color: var(--edh-cookie-settings-text); + margin: auto; + max-height: calc(100% - 128px); + max-width: min(896px, calc(100% - 8px)); + outline: none; + width: 100%; +} + +.edh-cookie-settings[open] { + display: flex; + flex-direction: column; +} + +.edh-cookie-settings::backdrop { + background: rgb(0 0 0 / 50%); +} + +html:has(.edh-cookie-settings[open]) { + overflow: hidden; +} + +.edh-cookie-settings__body { + display: flex; + flex: 1; + flex-direction: column; + gap: 32px; + overflow-y: auto; + padding: 24px 20px; +} + +.edh-cookie-settings__intro { + line-height: 28px; +} + +.edh-cookie-settings__category { + display: grid; + gap: 16px; +} + +.edh-cookie-settings__category h3 { + color: inherit; + font-size: 20px; + font-weight: 600; + line-height: 28px; + margin-bottom: 12px; +} + +.edh-cookie-settings__category p { + font-size: 14px; + line-height: 20px; +} + +.edh-cookie-settings__switch { + align-items: center; + cursor: pointer; + display: inline-flex; + justify-self: start; + position: relative; + user-select: none; +} + +.edh-cookie-settings__switch:has(input:disabled) { + opacity: 0.5; + pointer-events: none; +} + +.edh-cookie-settings__switch input { + cursor: pointer; + height: 100%; + inset: 0; + opacity: 0; + position: absolute; + width: 100%; +} + +.edh-cookie-settings__track { + align-items: center; + background: var(--edh-cookie-settings-switch-off); + border-radius: 9999px; + display: inline-flex; + height: 28px; + margin-inline-end: 8px; + padding: 0 4px; + transition: background 250ms; + width: 48px; +} + +.edh-cookie-settings__track::before { + background: #fff; + border-radius: 9999px; + box-shadow: + 0 0 5px 0 rgb(0 0 0 / 2%), + 0 2px 10px 0 rgb(0 0 0 / 6%), + 0 0 1px 0 rgb(0 0 0 / 30%); + content: ""; + height: 20px; + transition: margin 250ms cubic-bezier(0.4, 0, 0.2, 1); + width: 20px; +} + +.edh-cookie-settings__switch input:checked + .edh-cookie-settings__track { + background: var(--edh-cookie-settings-switch); +} + +.edh-cookie-settings__switch input:checked + .edh-cookie-settings__track::before { + margin-inline-start: 20px; +} + +.edh-cookie-settings__switch input:focus-visible + .edh-cookie-settings__track, +.edh-cookie-settings__button:focus-visible { + outline: 2px solid var(--edh-cookie-settings-focus); + outline-offset: 2px; +} + +.edh-cookie-settings__footer { + border-top: 1px solid #e7ecf6; + display: flex; + gap: 8px; + justify-content: flex-end; + padding: 16px 20px; +} + +.edh-cookie-settings__button { + border: 2px solid currentcolor; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + height: 40px; + line-height: 20px; + min-width: 80px; + padding: 0 16px; + transition: transform 250ms, opacity 250ms; + white-space: nowrap; +} + +.edh-cookie-settings__button:hover { + opacity: 0.8; +} + +.edh-cookie-settings__button:active { + transform: scale(0.97); +} + +.edh-cookie-settings__button--deny { + color: var(--edh-cookie-settings-danger); +} + +.edh-cookie-settings__button--accept { + color: var(--edh-cookie-settings-violet); +} + +.edh-cookie-settings__button--save { + background: var(--edh-cookie-settings-violet); + border: 0; + color: #fff; + font-weight: 600; +} + +@media (min-width: 640px) { + .edh-cookie-settings { + max-width: min(896px, calc(100% - 48px)); + } + + .edh-cookie-settings__body, + .edh-cookie-settings__footer { + padding-left: 32px; + padding-right: 32px; + } + + .edh-cookie-settings__category h3 { + font-size: 24px; + line-height: 32px; + } +} + +@media (min-width: 768px) { + .edh-cookie-settings__category { + align-items: center; + grid-template-columns: minmax(0, 1fr) 152px; + } +} diff --git a/source/_static/matomo.js b/source/_static/matomo.js index 8113bbb..915074c 100644 --- a/source/_static/matomo.js +++ b/source/_static/matomo.js @@ -12,12 +12,97 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Mirrors the webportal's useTrackingConsent and useMatomo hooks. (function () { + var TRACKING_CONSENT_STORAGE_KEY = 'edh.trackingConsent'; + var containerUrl = document.currentScript.getAttribute('data-matomo-container-url'); - function enableTracking() { + function getStoredTrackingConsent() { + try { + var storedConsent = window.localStorage.getItem(TRACKING_CONSENT_STORAGE_KEY); + return storedConsent === 'accepted' || storedConsent === 'denied' ? storedConsent : null; + } catch (error) { + return null; + } + } + + function storeTrackingConsent(consent) { + try { + window.localStorage.setItem(TRACKING_CONSENT_STORAGE_KEY, consent); + } catch (error) { + // Consent still applies for the current page when storage is unavailable. + } + } + + function applyMatomoTrackingConsent(consent) { var queue = window._paq = window._paq || []; - queue.push(['setConsentGiven']); + + if (consent === 'accepted') { + queue.push(['setConsentGiven']); + return; + } + + queue.push(['forgetConsentGiven']); + queue.push(['deleteCookies']); + } + + // Matomo Tag Manager creates the pageview tracker while the container loads, + // so reapply stored consent after load to reach the live tracker. + function reapplyStoredConsent() { + if (getStoredTrackingConsent() === 'accepted') { + applyMatomoTrackingConsent('accepted'); + } + } + + function loadMatomoContainer() { + var dataLayer = window._mtm = window._mtm || []; + dataLayer.push({'mtm.startTime': new Date().getTime(), event: 'mtm.Start'}); + + var script = document.createElement('script'); + script.async = true; + script.src = containerUrl; + script.onload = reapplyStoredConsent; + document.head.appendChild(script); + } + + function openCookieSettings() { + var dialog = document.querySelector('[data-cookie-settings]'); + var trackingSwitch = dialog.querySelector('[data-cookie-settings-tracking]'); + var trackingLabel = dialog.querySelector('[data-cookie-settings-tracking-label]'); + + function saveTrackingConsent(isTrackingCookiesEnabled) { + var consent = isTrackingCookiesEnabled ? 'accepted' : 'denied'; + + dialog.close(); + applyMatomoTrackingConsent(consent); + storeTrackingConsent(consent); + } + + trackingSwitch.addEventListener('change', function () { + trackingLabel.textContent = trackingSwitch.checked ? 'Enabled' : 'Disabled'; + }); + dialog.querySelector('[data-cookie-settings-deny]').addEventListener('click', function () { + saveTrackingConsent(false); + }); + dialog.querySelector('[data-cookie-settings-accept]').addEventListener('click', function () { + saveTrackingConsent(true); + }); + dialog.querySelector('[data-cookie-settings-save]').addEventListener('click', function () { + saveTrackingConsent(trackingSwitch.checked); + }); + document.addEventListener('keydown', function (event) { + if (dialog.open && event.key === 'Escape' && !event.isComposing) { + event.preventDefault(); + saveTrackingConsent(trackingSwitch.checked); + } + }); + dialog.addEventListener('cancel', function (event) { + event.preventDefault(); + }); + + dialog.showModal(); + dialog.focus(); } function markCopyButtons() { @@ -29,15 +114,19 @@ }); } - document.addEventListener('DOMContentLoaded', markCopyButtons); + window.addEventListener('DOMContentLoaded', markCopyButtons); - enableTracking(); - var dataLayer = window._mtm = window._mtm || []; - dataLayer.push({'mtm.startTime': new Date().getTime(), event: 'mtm.Start'}); + var storedConsent = getStoredTrackingConsent(); + + if (storedConsent) { + applyMatomoTrackingConsent(storedConsent); + } - var script = document.createElement('script'); - script.async = true; - script.src = containerUrl; - script.onload = enableTracking; - document.head.appendChild(script); + if (containerUrl) { + loadMatomoContainer(); + } + + if (!storedConsent) { + openCookieSettings(); + } }()); diff --git a/source/_templates/partials/cookie-settings-modal.html b/source/_templates/partials/cookie-settings-modal.html new file mode 100644 index 0000000..e173806 --- /dev/null +++ b/source/_templates/partials/cookie-settings-modal.html @@ -0,0 +1,61 @@ + + + + + +
+ + + +
+
diff --git a/source/_templates/partials/site-foot.html b/source/_templates/partials/site-foot.html index fe70ad5..708ba27 100644 --- a/source/_templates/partials/site-foot.html +++ b/source/_templates/partials/site-foot.html @@ -15,3 +15,4 @@ --> {% include "partials/portal-footer.html" %} +{% include "partials/cookie-settings-modal.html" %} diff --git a/source/conf.py b/source/conf.py index 201a68d..b3bf17c 100644 --- a/source/conf.py +++ b/source/conf.py @@ -175,13 +175,17 @@ "notebook-gallery.css", "badges.css", "search-icons.css", + "cookie-settings.css", ] -html_js_files = ["portal-frame.js", "search-icons.js"] matomo_container_url = os.getenv("MATOMO_TAG_MANAGER_CONTAINER_URL", "").strip() -if matomo_container_url: - html_js_files.append( - ("matomo.js", {"data-matomo-container-url": matomo_container_url}) - ) +html_js_files = [ + "portal-frame.js", + "search-icons.js", + ( + "matomo.js", + {"data-matomo-container-url": matomo_container_url, "priority": 400}, + ), +] html_context = { "portal_url": BASE_URL, From e33de03cda39e86bb11fc1796074f4aae6ffd4fb Mon Sep 17 00:00:00 2001 From: Alfredo Toselli Date: Thu, 22 Oct 2026 15:18:56 +0200 Subject: [PATCH 2/3] style: ease animation --- source/_static/cookie-settings.css | 29 +++++++++++++++++++++++++++++ source/_static/matomo.js | 5 ++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/source/_static/cookie-settings.css b/source/_static/cookie-settings.css index 31e05df..9aa74f7 100644 --- a/source/_static/cookie-settings.css +++ b/source/_static/cookie-settings.css @@ -38,14 +38,37 @@ } .edh-cookie-settings[open] { + animation: + edh-cookie-settings-fade 300ms ease-out, + edh-cookie-settings-slide 400ms ease; display: flex; flex-direction: column; } .edh-cookie-settings::backdrop { + animation: edh-cookie-settings-fade 300ms ease-out; background: rgb(0 0 0 / 50%); } +/* HeroUI's modal enter animation */ +@keyframes edh-cookie-settings-fade { + from { + opacity: 0; + } +} + +@keyframes edh-cookie-settings-slide { + from { + translate: 0 80px; + } +} + +@keyframes edh-cookie-settings-scale { + from { + scale: 1.03; + } +} + html:has(.edh-cookie-settings[open]) { overflow: hidden; } @@ -192,6 +215,12 @@ html:has(.edh-cookie-settings[open]) { max-width: min(896px, calc(100% - 48px)); } + .edh-cookie-settings[open] { + animation: + edh-cookie-settings-fade 300ms ease-out, + edh-cookie-settings-scale 300ms ease-out; + } + .edh-cookie-settings__body, .edh-cookie-settings__footer { padding-left: 32px; diff --git a/source/_static/matomo.js b/source/_static/matomo.js index 915074c..db4ac5d 100644 --- a/source/_static/matomo.js +++ b/source/_static/matomo.js @@ -127,6 +127,9 @@ } if (!storedConsent) { - openCookieSettings(); + new PerformanceObserver(function (list, observer) { + observer.disconnect(); + openCookieSettings(); + }).observe({type: 'paint', buffered: true}); } }()); From 31482bc0acd456879cc726032ede77d9c08cc139 Mon Sep 17 00:00:00 2001 From: Alfredo Toselli Date: Thu, 22 Oct 2026 16:13:56 +0200 Subject: [PATCH 3/3] fixup! feat: cookie modal --- .claude/launch.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .claude/launch.json diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index 37800e7..0000000 --- a/.claude/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "docs", - "runtimeExecutable": "make", - "runtimeArgs": ["serve"], - "port": 8000 - } - ] -}