@koehler8/cms-ext-compliance 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,497 @@
1
+ <template>
2
+ <section class="legal-page section-shell" data-analytics-section="cookies">
3
+ <div class="container legal-page__container">
4
+ <article class="legal-card">
5
+ <header class="legal-card__header">
6
+ <h2 class="legal-card__title">{{ headerTitle }}</h2>
7
+ <span class="legal-divider" aria-hidden="true"></span>
8
+ <p class="legal-card__meta">Last revised: {{ lastUpdated }}</p>
9
+ </header>
10
+
11
+ <div class="legal-content">
12
+ <section class="consent-panel">
13
+ <h3 class="consent-panel__title">Cookie Consent Management</h3>
14
+ <p>We respect your privacy. When the cookie consent banner is enabled, you'll see a small prompt on the site to accept or decline analytics tracking. Regardless of the banner, you can always manage your preference here.</p>
15
+ <p><strong>Current Status:</strong> <span :class="['consent-status', consentStatusClass]">{{ consentStatusText }}</span></p>
16
+ <p v-if="isAccepted">
17
+ You have accepted cookies and analytics tracking. Use the controls below to revoke that consent whenever you choose.
18
+ </p>
19
+ <p v-else-if="isDeclined">
20
+ You have declined cookies and analytics tracking. You can opt back in at any time using the controls below.
21
+ </p>
22
+ <p v-else>
23
+ You have not yet made a decision about cookies.
24
+ <template v-if="bannerEnabled">
25
+ The consent notice will continue to appear while you browse until you choose an option.
26
+ </template>
27
+ <template v-else>
28
+ Use the controls on this page whenever you are ready to accept or decline.
29
+ </template>
30
+ </p>
31
+ <div class="consent-controls">
32
+ <button
33
+ class="primary-button consent-button"
34
+ :disabled="isAccepted"
35
+ type="button"
36
+ @click="handleAccept"
37
+ >
38
+ Accept Analytics
39
+ </button>
40
+ <button
41
+ class="consent-button consent-button--decline"
42
+ :disabled="isDeclined"
43
+ type="button"
44
+ @click="handleDecline"
45
+ >
46
+ Decline Analytics
47
+ </button>
48
+ </div>
49
+ </section>
50
+ <template v-if="hasCustomContent">
51
+ <p v-if="customIntro">{{ customIntro }}</p>
52
+ <div
53
+ v-if="customBody"
54
+ class="legal-rich-text"
55
+ v-html="customBody"
56
+ />
57
+ <section
58
+ v-for="(section, index) in customSections"
59
+ :key="section.id || index"
60
+ class="legal-section"
61
+ >
62
+ <h3 v-if="section.title">{{ section.title }}</h3>
63
+ <p v-if="section.summary">{{ section.summary }}</p>
64
+ <div
65
+ v-if="section.body"
66
+ class="legal-rich-text"
67
+ v-html="section.body"
68
+ />
69
+ <p
70
+ v-for="(paragraph, paragraphIndex) in section.paragraphs"
71
+ :key="`paragraph-${paragraphIndex}`"
72
+ >
73
+ {{ paragraph }}
74
+ </p>
75
+ <ul v-if="section.list.length">
76
+ <li
77
+ v-for="(item, itemIndex) in section.list"
78
+ :key="`item-${itemIndex}`"
79
+ >
80
+ {{ item }}
81
+ </li>
82
+ </ul>
83
+ </section>
84
+ </template>
85
+ <template v-else>
86
+ <p>About this Policy</p>
87
+ <p>Our Privacy Policy explains our principles when it comes to the collection, processing, and
88
+ storage of your personal information. This policy explains the use of cookies in more details,
89
+ such as what cookies are and how they are used. However, to get a full picture of how we
90
+ handle your privacy this policy should be read together with our Privacy Policy.</p>
91
+ <p>What are cookies?</p>
92
+ <p>Cookies are text files, containing small amounts of information, which are downloaded to
93
+ your browsing device, such as your computer, mobile device or smartphone, when you visit
94
+ our website or use our services. Cookies can be recognised by the website that downloaded
95
+ them — or other websites that use the same cookies. This helps websites know if the
96
+ browsing device has visited them before.</p>
97
+ <p>We use two types of cookies: persistent cookies and session cookies. A persistent cookie
98
+ lasts beyond the current session and is used for many purposes, such as recognizing you as
99
+ an existing user, so it’s easier to return to us and interact with our services. Since a
100
+ persistent cookie stays in your browser, it will be read by us when you return to one of our
101
+ sites or visit a third-party site that uses our services. Session cookies last only as long as the
102
+ session (usually the current visit to a website or a browser session).</p>
103
+ <p>Do I need to accept cookies?</p>
104
+ <p>No, you do not need to accept cookies. But, please be advised that if you do not accept
105
+ cookies the service might be difficult or impossible to use.</p>
106
+ <p>You can adjust settings on your browser so that you will be notified when you receive a
107
+ cookie. Please refer to your browser documentation to check if cookies have been enabled
108
+ on your computer or to request not to receive cookies. As cookies allow you to take
109
+ advantage of some of the Website’s essential features, we recommend that you accept
110
+ cookies. For instance, if you block or otherwise reject our cookies, you will not be able to use
111
+ any products or services on the website that may require you to log in.</p>
112
+ <p>What are the cookies used for?</p>
113
+ <p>Functional Cookies Functional cookies are essential to provide our services as we want to
114
+ provide them. They are used to remember your preferences on our website and to provide
115
+ an enhanced and personalised experience. The information collected by these cookies is
116
+ usually anonymised, so we cannot identify you personally. Functional cookies do not track
117
+ your internet usage or gather information that could be used for selling advertising. These
118
+ cookies are usually session cookies that will expire when you close your browsing session,
119
+ but some are also persistent cookies.</p>
120
+ <p>Essential or ‘Strictly Necessary’ Cookies</p>
121
+ <p>These cookies are essential to provide our services. Without these cookies, parts of our
122
+ website will not function. These cookies do not track where you have been on the internet
123
+ and do remember preferences beyond your current visit and do not gather information about
124
+ you that could be used for marketing purposes. These cookies are usually session cookies
125
+ which will expire when you close your browsing session.</p>
126
+ <p>Analytical Performance Cookies</p>
127
+ <p>Analytical performance cookies are used to monitor the performance of our website and
128
+ services, for example, to determine the number of page views and the number of unique
129
+ users a website has. Web analytics services may be designed and operated by third parties.
130
+ The information provided by these cookies allows us to analyse patterns of user behaviour
131
+ and we use that information to enhance user experience or identify areas of the website
132
+ which may require maintenance. The information is anonymous, cannot be used to identify
133
+ you, does not contain personal information and is only used for statistical purposes.</p>
134
+ <p>Advertising Cookies</p>
135
+ <p>These cookies remember that you have visited a website and use that information to provide
136
+ you with content or advertising which is tailored to your interests. They are also used to limit
137
+ the number of times you see an advertisement as well as help measure the effectiveness of
138
+ the advertising campaign. The information collected by these cookies may be shared with
139
+ trusted third-party partners such as advertisers.</p>
140
+ <p>We may update this Cookie Policy from time to time for operational, legal or regulatory
141
+ reasons.</p>
142
+ <p>If you have any questions regarding our policy on cookies please contact: {{ supportEmail }}</p>
143
+ </template>
144
+ </div>
145
+ </article>
146
+ </div>
147
+ </section>
148
+ </template>
149
+
150
+ <script setup>
151
+ import { computed, inject, onMounted, onUnmounted, ref } from 'vue';
152
+
153
+ import {
154
+ getConsentStatus,
155
+ acceptConsent,
156
+ declineConsent,
157
+ ConsentStatus,
158
+ isCookieBannerEnabled,
159
+ scheduleAnalyticsLoad
160
+ } from '@koehler8/cms/utils/cookieConsent';
161
+ import { trackEvent } from '@koehler8/cms/utils/analytics';
162
+
163
+ const props = defineProps({
164
+ content: {
165
+ type: Object,
166
+ default: null,
167
+ },
168
+ configKey: {
169
+ type: String,
170
+ default: null,
171
+ },
172
+ });
173
+
174
+ const injectedSiteData = inject('siteData', ref({}));
175
+
176
+ const siteName = computed(() => injectedSiteData.value?.site?.title || '');
177
+ const siteUrl = computed(() => injectedSiteData.value?.site?.url || '');
178
+ const supportEmail = computed(() => injectedSiteData.value?.site?.supportEmail || '');
179
+ const customContent = computed(() =>
180
+ props.content && typeof props.content === 'object' ? props.content : null,
181
+ );
182
+ const consentStatus = ref(ConsentStatus.PENDING);
183
+ const bannerEnabled = isCookieBannerEnabled();
184
+ const TOKEN_TICKER = '';
185
+ const googleId = computed(() => injectedSiteData.value?.site?.googleId || '');
186
+
187
+ const headerTitle = computed(() => {
188
+ if (customContent.value?.title) {
189
+ return customContent.value.title;
190
+ }
191
+ if (siteName.value) {
192
+ return `${siteName.value} - Cookies Policy`;
193
+ }
194
+ return 'Cookies Policy';
195
+ });
196
+
197
+ const lastUpdated = computed(() => customContent.value?.lastUpdated || 'August 2025');
198
+ const customIntro = computed(() =>
199
+ typeof customContent.value?.intro === 'string' ? customContent.value.intro : '',
200
+ );
201
+ const customBody = computed(() =>
202
+ typeof customContent.value?.body === 'string' ? customContent.value.body : '',
203
+ );
204
+
205
+ function normalizeSectionItems(sections) {
206
+ if (!Array.isArray(sections)) return [];
207
+ return sections
208
+ .map((section, index) => {
209
+ if (!section || typeof section !== 'object') return null;
210
+ const paragraphs = Array.isArray(section.paragraphs)
211
+ ? section.paragraphs.filter((text) => typeof text === 'string' && text.trim().length)
212
+ : [];
213
+ const list = Array.isArray(section.list)
214
+ ? section.list.filter((text) => typeof text === 'string' && text.trim().length)
215
+ : [];
216
+ const summary = typeof section.summary === 'string' ? section.summary : null;
217
+ const body = typeof section.body === 'string' ? section.body : null;
218
+ const html = typeof section.html === 'string' ? section.html : null;
219
+ const title = typeof section.title === 'string' ? section.title : '';
220
+ if (!title && !summary && !body && !html && !paragraphs.length && !list.length) {
221
+ return null;
222
+ }
223
+ return {
224
+ id: section.id || title || `section-${index}`,
225
+ title,
226
+ summary,
227
+ body: body || html,
228
+ paragraphs,
229
+ list,
230
+ };
231
+ })
232
+ .filter(Boolean);
233
+ }
234
+
235
+ const customSections = computed(() => normalizeSectionItems(customContent.value?.sections));
236
+ const hasCustomContent = computed(
237
+ () => Boolean(customIntro.value || customBody.value || customSections.value.length),
238
+ );
239
+
240
+ const consentStatusText = computed(() => {
241
+ switch (consentStatus.value) {
242
+ case ConsentStatus.ACCEPTED:
243
+ return 'Accepted';
244
+ case ConsentStatus.DECLINED:
245
+ return 'Declined';
246
+ default:
247
+ return 'Pending';
248
+ }
249
+ });
250
+
251
+ const consentStatusClass = computed(() => {
252
+ switch (consentStatus.value) {
253
+ case ConsentStatus.ACCEPTED:
254
+ return 'consent-status--accepted';
255
+ case ConsentStatus.DECLINED:
256
+ return 'consent-status--declined';
257
+ default:
258
+ return 'consent-status--pending';
259
+ }
260
+ });
261
+
262
+ const isAccepted = computed(() => consentStatus.value === ConsentStatus.ACCEPTED);
263
+ const isDeclined = computed(() => consentStatus.value === ConsentStatus.DECLINED);
264
+
265
+ let consentChangeHandler;
266
+
267
+ async function initializeAnalyticsIfNeeded() {
268
+ if (!googleId.value) return;
269
+ try {
270
+ await scheduleAnalyticsLoad(googleId.value);
271
+ } catch (error) {
272
+ console.error('Failed to initialize analytics after consent change:', error);
273
+ }
274
+ }
275
+
276
+ function applyConsent(status) {
277
+ consentStatus.value = status;
278
+ }
279
+
280
+ async function handleAccept() {
281
+ if (isAccepted.value) return;
282
+ acceptConsent();
283
+ applyConsent(ConsentStatus.ACCEPTED);
284
+ trackEvent('cookie_consent_choice', {
285
+ token: TOKEN_TICKER || 'unknown',
286
+ choice: 'accept',
287
+ source: 'settings'
288
+ });
289
+ await initializeAnalyticsIfNeeded();
290
+ }
291
+
292
+ function handleDecline() {
293
+ if (isDeclined.value) return;
294
+ declineConsent();
295
+ applyConsent(ConsentStatus.DECLINED);
296
+ trackEvent('cookie_consent_choice', {
297
+ token: TOKEN_TICKER || 'unknown',
298
+ choice: 'decline',
299
+ source: 'settings'
300
+ });
301
+ }
302
+
303
+ onMounted(() => {
304
+ consentStatus.value = getConsentStatus();
305
+ consentChangeHandler = (event) => {
306
+ const nextStatus = event?.detail?.status;
307
+ if (nextStatus && Object.values(ConsentStatus).includes(nextStatus)) {
308
+ consentStatus.value = nextStatus;
309
+ } else {
310
+ consentStatus.value = getConsentStatus();
311
+ }
312
+ };
313
+ window.addEventListener('consentChanged', consentChangeHandler);
314
+ });
315
+
316
+ onUnmounted(() => {
317
+ if (consentChangeHandler) {
318
+ window.removeEventListener('consentChanged', consentChangeHandler);
319
+ consentChangeHandler = undefined;
320
+ }
321
+ });
322
+ </script>
323
+
324
+
325
+
326
+ <style scoped>
327
+ .legal-page {
328
+ background: var(
329
+ --legal-page-bg,
330
+ color-mix(in srgb, var(--brand-bg-900, #04050a) 90%, #020207 10%)
331
+ );
332
+ }
333
+
334
+ .legal-page__container {
335
+ max-width: 920px;
336
+ margin: 0 auto;
337
+ }
338
+
339
+ .legal-card {
340
+ --legal-card-color: var(
341
+ --legal-card-text,
342
+ var(--brand-card-text, var(--ui-text-primary, #1f2a44))
343
+ );
344
+ --legal-card-color-muted: color-mix(in srgb, var(--legal-card-color) 70%, transparent);
345
+ --legal-card-color-subtle: color-mix(in srgb, var(--legal-card-color) 55%, transparent);
346
+ background: var(--legal-card-bg, var(--brand-surface-card-bg, #ffffff));
347
+ color: var(--legal-card-color);
348
+ border-radius: var(--brand-card-radius, 28px);
349
+ border: 1px solid
350
+ color-mix(in srgb, var(--brand-surface-card-border, rgba(78, 105, 155, 0.35)) 80%, transparent);
351
+ box-shadow: var(--legal-card-shadow, 0 35px 70px rgba(4, 6, 15, 0.35));
352
+ padding: clamp(28px, 6vw, 56px);
353
+ }
354
+
355
+ .legal-card__header {
356
+ text-align: center;
357
+ margin-bottom: clamp(20px, 4vw, 36px);
358
+ }
359
+
360
+ .legal-card__title {
361
+ margin: 0;
362
+ font-size: clamp(1.6rem, 4vw, 2.4rem);
363
+ letter-spacing: 0.14em;
364
+ text-transform: uppercase;
365
+ color: var(--legal-card-color, currentColor);
366
+ }
367
+
368
+ .legal-card__meta {
369
+ margin: 14px 0 0;
370
+ font-size: 0.78rem;
371
+ letter-spacing: 0.2em;
372
+ text-transform: uppercase;
373
+ color: var(
374
+ --legal-card-meta-color,
375
+ var(--legal-card-color-subtle, color-mix(in srgb, var(--ui-text-muted, rgba(31, 42, 68, 0.7)) 90%, transparent))
376
+ );
377
+ }
378
+
379
+ .legal-divider {
380
+ display: block;
381
+ width: 72px;
382
+ height: 2px;
383
+ margin: 18px auto 0;
384
+ background: var(
385
+ --legal-divider-color,
386
+ color-mix(in srgb, var(--brand-border-highlight, rgba(79, 108, 240, 0.6)) 90%, transparent)
387
+ );
388
+ border-radius: 999px;
389
+ }
390
+
391
+ .legal-content {
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 1rem;
395
+ line-height: 1.7;
396
+ color: inherit;
397
+ }
398
+
399
+ .legal-content p {
400
+ margin: 0;
401
+ }
402
+
403
+ .legal-content strong {
404
+ color: var(--legal-strong-color, var(--legal-card-color));
405
+ }
406
+
407
+ .legal-content ul {
408
+ margin: 0;
409
+ padding-left: 1.2rem;
410
+ line-height: 1.6;
411
+ }
412
+
413
+ .legal-content li + li {
414
+ margin-top: 0.35rem;
415
+ }
416
+
417
+ .consent-panel {
418
+ background: var(
419
+ --consent-panel-bg,
420
+ color-mix(in srgb, var(--brand-card-soft, rgba(10, 12, 18, 0.92)) 92%, transparent)
421
+ );
422
+ color: var(--consent-panel-color, var(--legal-card-color));
423
+ border: 1px solid
424
+ var(
425
+ --consent-panel-border,
426
+ color-mix(
427
+ in srgb,
428
+ var(--brand-surface-card-border, rgba(79, 108, 240, 0.3)) 85%,
429
+ transparent
430
+ )
431
+ );
432
+ border-radius: clamp(18px, 3vw, 26px);
433
+ padding: clamp(20px, 4vw, 32px);
434
+ margin-bottom: clamp(24px, 4vw, 40px);
435
+ }
436
+
437
+ .consent-panel__title {
438
+ margin-top: 0;
439
+ margin-bottom: 0.75rem;
440
+ font-size: 1.05rem;
441
+ letter-spacing: 0.08em;
442
+ text-transform: uppercase;
443
+ color: var(--consent-panel-title-color, var(--consent-panel-color));
444
+ }
445
+
446
+ .consent-status {
447
+ font-weight: 700;
448
+ letter-spacing: 0.1em;
449
+ text-transform: uppercase;
450
+ margin-left: 6px;
451
+ }
452
+
453
+ .consent-status--accepted {
454
+ color: var(--brand-status-success, #1eb980);
455
+ }
456
+
457
+ .consent-status--declined {
458
+ color: var(--brand-status-error, #e45865);
459
+ }
460
+
461
+ .consent-status--pending {
462
+ color: var(--ui-text-muted, rgba(31, 42, 68, 0.72));
463
+ }
464
+
465
+ .consent-controls {
466
+ display: flex;
467
+ flex-wrap: wrap;
468
+ gap: 12px;
469
+ margin-top: 16px;
470
+ }
471
+
472
+ .consent-button {
473
+ border-radius: 999px;
474
+ padding: 0.85rem 1.5rem;
475
+ font-size: 0.85rem;
476
+ letter-spacing: 0.12em;
477
+ text-transform: uppercase;
478
+ }
479
+
480
+ .consent-button:disabled {
481
+ opacity: 0.55;
482
+ cursor: not-allowed;
483
+ }
484
+
485
+ .consent-button--decline {
486
+ border: 1px solid color-mix(in srgb, var(--brand-border-highlight, rgba(79, 108, 240, 0.45)) 90%, transparent);
487
+ color: var(--consent-panel-color, var(--legal-card-color));
488
+ background: transparent;
489
+ transition: color 0.2s ease, border-color 0.2s ease;
490
+ }
491
+
492
+ .consent-button--decline:hover,
493
+ .consent-button--decline:focus-visible {
494
+ border-color: var(--brand-border-highlight, rgba(79, 108, 240, 0.8));
495
+ color: var(--brand-cta-text, #ffffff);
496
+ }
497
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <section class="legal-bar ui-legal">
3
+ <div class="container ui-legal__content">
4
+ <div class="legal-bar__copy">
5
+ <small>{{ copyrightText }}</small>
6
+ </div>
7
+ <ul class="ui-legal__links">
8
+ <li v-for="link in legalLinks" :key="link.href">
9
+ <a class="ui-legal__link" :href="link.href" rel="nofollow">
10
+ {{ link.label }}
11
+ </a>
12
+ </li>
13
+ </ul>
14
+ </div>
15
+ </section>
16
+ </template>
17
+
18
+ <script setup>
19
+ import { computed, inject, ref } from 'vue';
20
+
21
+ const injectedSiteData = inject('siteData', ref({}));
22
+
23
+ const siteName = computed(() => injectedSiteData.value?.site?.title || '');
24
+ const currentYear = new Date().getFullYear();
25
+ const copyrightText = computed(() => {
26
+ const brand = siteName.value ? ` ${siteName.value}` : '';
27
+ return `© ${currentYear}${brand} - All rights reserved.`;
28
+ });
29
+
30
+ const legalLinks = [
31
+ { href: '/terms', label: 'Terms' },
32
+ { href: '/privacy', label: 'Privacy' },
33
+ { href: '/cookies', label: 'Cookies' },
34
+ ];
35
+ </script>
36
+
37
+ <style scoped>
38
+ .legal-bar__copy {
39
+ font-size: 0.85rem;
40
+ color: inherit;
41
+ }
42
+ </style>