diff --git a/src/App.svelte b/src/App.svelte
index 8cfb4d9..f74e6db 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -4,11 +4,13 @@
import Services from "./components/Services/Services.svelte";
import Footer from "./components/Footer/Footer.svelte";
import Header from "./components/Header/Header.svelte";
+ import CookiePopUp from "./components/CookiePopUp/CookiePopUp.svelte";
import Contacts from "./pages/Contacts.svelte";
import Forbidden from "./pages/403.svelte";
import ServerError from "./pages/500.svelte";
import ErrorGeneric from "./pages/errore.svelte";
import NotFound from "./pages/404.svelte";
+ import { initializeStoredConsent } from "./lib/cookieConsent";
let pathname = '/'
@@ -31,10 +33,12 @@
return '404'
}
- $: currentRoute = getRoute(pathname)
- $: isCenteredMain = ['403', '404', '500', 'error'].includes(currentRoute)
+ let currentRoute = $derived(getRoute(pathname))
+ let isCenteredMain = $derived(['403', '404', '500', 'error'].includes(currentRoute))
onMount(() => {
+ initializeStoredConsent()
+
try {
const saved = localStorage.getItem('theme')
if (saved) {
@@ -79,4 +83,5 @@
{/if}
+
\ No newline at end of file
diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css
index 7972986..e99c059 100644
--- a/src/components/Button/Button.css
+++ b/src/components/Button/Button.css
@@ -13,6 +13,8 @@
text-decoration: none;
border: none;
box-shadow: inset 0 0 0 var(--button-border-width) var(--primary-color);
+ font-family: 'IBM Plex Mono', monospace;
+ white-space: nowrap;
}
.button:hover {
diff --git a/src/components/Button/Button.svelte b/src/components/Button/Button.svelte
index c6226ed..e77551b 100644
--- a/src/components/Button/Button.svelte
+++ b/src/components/Button/Button.svelte
@@ -1,13 +1,16 @@
{#if href}
@@ -15,6 +18,7 @@
class="button"
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`}
{href}
+ {...restProps}
>
{text}
@@ -22,6 +26,7 @@
diff --git a/src/components/CookiePopUp/CookiePopUp.css b/src/components/CookiePopUp/CookiePopUp.css
new file mode 100644
index 0000000..bf08661
--- /dev/null
+++ b/src/components/CookiePopUp/CookiePopUp.css
@@ -0,0 +1,140 @@
+.cookie-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 199;
+ background: rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(2px);
+}
+
+.cookie-popup {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 50px;
+ z-index: 200;
+ position: fixed;
+ left: 50%;
+ bottom: 25px;
+ transform: translateX(-50%);
+ width: min(100%, 1100px);
+ max-width: calc(100% - 25px * 2);
+ padding: 50px;
+ border-radius: 5px;
+ background-color: var(--surface);
+ color: var(--text-color);
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
+ border: 2px solid rgba(127, 127, 127, 0.3);
+}
+
+.cookie-popup-container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.cookie-popup-content p {
+ margin: 10px 0 15px;
+}
+
+.cookie-popup-title {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.cookie-popup-title h2 {
+ font-size: 1.25rem;
+ margin: 0;
+}
+
+.cookie-icon {
+ width: 30px;
+ height: 30px;
+ display: inline-block;
+ background-color: var(--text-color);
+ -webkit-mask-image: url('./assets/cookie.svg');
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: center;
+ -webkit-mask-size: contain;
+ mask-image: url('./assets/cookie.svg');
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ transition: background-color 0.2s ease;
+}
+
+.cookie-popup-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 50px;
+ flex-wrap: nowrap;
+}
+
+.cookie-link {
+ color: var(--primary-color);
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.cookie-link-button {
+ background: transparent;
+ border: 0;
+ padding: 0;
+ font: inherit;
+}
+
+.cookie-link:hover {
+ text-decoration: underline;
+}
+
+.cookie-popup-buttons {
+ display: flex;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.cookie-popup-buttons .button {
+ white-space: nowrap;
+ flex-shrink: 0;
+ min-width: fit-content;
+}
+
+.cookie-selector p {
+ margin: 0 0 5px;
+}
+
+.cookie-toggle-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 50px;
+ margin-bottom: 12px;
+}
+
+/* toggle item styling moved to Toggle.css */
+
+@media (max-width: 640px) {
+ .cookie-popup {
+ bottom: 12px;
+ padding: 16px;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 16px;
+ }
+
+ .cookie-popup-actions {
+ justify-content: stretch;
+ gap: 12px;
+ flex-direction: column;
+ }
+
+ .cookie-popup-actions .button,
+ .cookie-link {
+ flex: 1 1 100%;
+ text-align: center;
+ }
+
+ .cookie-link-button {
+ padding: 10px 0;
+ }
+}
diff --git a/src/components/CookiePopUp/CookiePopUp.svelte b/src/components/CookiePopUp/CookiePopUp.svelte
new file mode 100644
index 0000000..e4d2d50
--- /dev/null
+++ b/src/components/CookiePopUp/CookiePopUp.svelte
@@ -0,0 +1,153 @@
+
+
+
+
+{#if isOpen}
+
+
+
+{/if}
\ No newline at end of file
diff --git a/src/components/CookiePopUp/assets/cookie.svg b/src/components/CookiePopUp/assets/cookie.svg
new file mode 100644
index 0000000..6de4a0e
--- /dev/null
+++ b/src/components/CookiePopUp/assets/cookie.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/Toggle/Toggle.css b/src/components/Toggle/Toggle.css
new file mode 100644
index 0000000..5a7ee92
--- /dev/null
+++ b/src/components/Toggle/Toggle.css
@@ -0,0 +1,98 @@
+/* Toggle CSS variables (can be set on the element or a parent):
+ --toggle-default-checked: true | false
+ --toggle-default-disabled: true | false
+ --toggle-on-bg: color
+ --toggle-off-bg: color
+ --toggle-knob: color
+ --primary-color: fallback color for --toggle-on-bg
+ Size overrides: --toggle-width, --toggle-height
+*/
+
+.toggle {
+ background: transparent;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: flex-start;
+ gap: 12px;
+}
+
+.track {
+ --toggle-width: 44px;
+ --toggle-height: 24px;
+ width: var(--toggle-width);
+ height: var(--toggle-height);
+ background: var(--toggle-off-bg, var(--background-opaque));
+ border-radius: 999px;
+ position: relative;
+ transition: background 0.18s ease;
+}
+
+.knob {
+ width: calc(var(--toggle-height) - 6px);
+ height: calc(var(--toggle-height) - 6px);
+ background: var(--toggle-knob, #fff);
+ border-radius: 50%;
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
+ transition: transform 0.18s ease, background 0.18s ease;
+}
+
+.toggle.on .track {
+ background: var(--toggle-on-bg, var(--primary-color, #0b74ff));
+}
+
+.toggle.on .knob {
+ transform: translateX(calc(var(--toggle-width) - var(--toggle-height)));
+}
+
+.toggle.sm .track {
+ --toggle-width: 34px;
+ --toggle-height: 18px
+}
+
+.toggle.lg .track {
+ --toggle-width: 56px;
+ --toggle-height: 32px
+}
+
+.toggle:disabled {
+ cursor: not-allowed;
+ opacity: 0.6
+}
+
+.toggle-label-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.toggle-label {
+ display: block;
+ font-weight: 500;
+ color: var(--text-color);
+ font-size: 0.95rem;
+ text-align: left;
+}
+
+.toggle-description {
+ display: block;
+ font-size: 0.85rem;
+ color: var(--muted-color, rgba(0, 0, 0, 0.6));
+ line-height: 1.4;
+}
+
+.cookie-toggle-item {
+ cursor: pointer;
+ display: flex;
+ padding: 8px 0;
+ margin: 0;
+}
+
+.cookie-toggle-item .toggle {
+ flex: 1;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/components/Toggle/Toggle.svelte b/src/components/Toggle/Toggle.svelte
new file mode 100644
index 0000000..bf48422
--- /dev/null
+++ b/src/components/Toggle/Toggle.svelte
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/src/lib/cookieConsent.js b/src/lib/cookieConsent.js
new file mode 100644
index 0000000..9a2d87f
--- /dev/null
+++ b/src/lib/cookieConsent.js
@@ -0,0 +1,103 @@
+const STORAGE_KEY = 'cookie_consent'
+const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365
+
+const toBoolean = (value) => value === true
+
+const withRequiredDefaults = (preferences = {}) => ({
+ tecnici: true,
+ analitici: toBoolean(preferences.analitici),
+ profilazione: toBoolean(preferences.profilazione)
+})
+
+const normalizeConsent = (value) => {
+ if (!value) return null
+
+ if (value === 'accepted_all') {
+ return {
+ value: 'accepted_all',
+ preferences: { tecnici: true, analitici: true, profilazione: true }
+ }
+ }
+
+ if (value === 'rejected_all') {
+ return {
+ value: 'rejected_all',
+ preferences: { tecnici: true, analitici: false, profilazione: false }
+ }
+ }
+
+ try {
+ const parsed = JSON.parse(value)
+ if (parsed?.value !== 'custom') return null
+ return {
+ value: 'custom',
+ preferences: withRequiredDefaults(parsed.preferences)
+ }
+ } catch {
+ return null
+ }
+}
+
+const serializeConsent = (consent) => {
+ if (consent.value === 'accepted_all' || consent.value === 'rejected_all') {
+ return consent.value
+ }
+
+ return JSON.stringify({
+ value: 'custom',
+ preferences: withRequiredDefaults(consent.preferences)
+ })
+}
+
+const setCategoryCookie = (name, granted) => {
+ document.cookie = `${name}=${granted ? 'granted' : 'denied'}; path=/; max-age=${ONE_YEAR_SECONDS}; SameSite=Lax`
+}
+
+export const getStoredConsent = () => {
+ if (typeof window === 'undefined') return null
+
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ return normalizeConsent(raw)
+ } catch {
+ return null
+ }
+}
+
+export const applyConsent = (consent) => {
+ if (typeof window === 'undefined' || !consent) return
+
+ const preferences = withRequiredDefaults(consent.preferences)
+ const root = document.documentElement
+
+ root.dataset.cookieConsent = consent.value
+ root.dataset.cookieTecnici = 'granted'
+ root.dataset.cookieAnalitici = preferences.analitici ? 'granted' : 'denied'
+ root.dataset.cookieProfilazione = preferences.profilazione ? 'granted' : 'denied'
+
+ setCategoryCookie('cm_cookie_tecnici', true)
+ setCategoryCookie('cm_cookie_analitici', preferences.analitici)
+ setCategoryCookie('cm_cookie_profilazione', preferences.profilazione)
+
+ window.dispatchEvent(
+ new CustomEvent('cookie-consent-updated', {
+ detail: { value: consent.value, preferences }
+ })
+ )
+}
+
+export const saveConsent = (consent) => {
+ if (typeof window === 'undefined' || !consent) return
+
+ try {
+ localStorage.setItem(STORAGE_KEY, serializeConsent(consent))
+ } catch {
+ // no-op if storage is unavailable
+ }
+}
+
+export const initializeStoredConsent = () => {
+ const consent = getStoredConsent()
+ if (consent) applyConsent(consent)
+ return consent
+}
diff --git a/src/styles/global.css b/src/styles/global.css
index e519932..c993c09 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -5,6 +5,7 @@
--muted-color: #6b7280;
--surface: #ffffff;
--background: rgba(246, 249, 255, 0.5);
+ --background-opaque: rgba(0, 0, 0, 0.12);
}
/* Dark theme overrides (toggle by setting attribute on ) */
@@ -14,6 +15,7 @@
--muted-color: #9ca3af;
--surface: #000;
--background: rgba(0, 0, 0, 0.8);
+ --background-opaque: rgba(255, 255, 255, 0.12);
}
* {
@@ -90,6 +92,22 @@ h2 {
}
p, a {
-
font-family: Helvetica, 'Segoe UI', sans-serif;
-}
\ No newline at end of file
+}
+
+/* SVG Mask utility: responsive to theme */
+.svg-mask {
+ background-color: var(--text-color);
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: center;
+ -webkit-mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ transition: background-color 0.2s ease;
+}
+
+/* Usage: add data attribute with SVG path
+ Example:
+ Then use inline style: -webkit-mask-image: var(--mask-url); mask-image: var(--mask-url);
+ OR use a specific class that sets the mask-image */
\ No newline at end of file