@rayels/loi25-core 1.0.0
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.
- package/index.d.ts +29 -0
- package/index.js +208 -0
- package/package.json +32 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface Loi25Config {
|
|
2
|
+
lang?: 'fr' | 'en';
|
|
3
|
+
position?: 'top' | 'bottom';
|
|
4
|
+
theme?: 'light' | 'dark';
|
|
5
|
+
privacyPolicyUrl?: string;
|
|
6
|
+
poweredByLink?: boolean;
|
|
7
|
+
expiryDays?: number;
|
|
8
|
+
onAcceptAll?: () => void;
|
|
9
|
+
onAcceptNecessary?: () => void;
|
|
10
|
+
onRevoke?: () => void;
|
|
11
|
+
texts?: Loi25Texts;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Loi25Texts {
|
|
15
|
+
title: string;
|
|
16
|
+
message: string;
|
|
17
|
+
acceptAll: string;
|
|
18
|
+
acceptNecessary: string;
|
|
19
|
+
manage: string;
|
|
20
|
+
privacyPolicy: string;
|
|
21
|
+
poweredBy: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function init(config?: Loi25Config): void;
|
|
25
|
+
export function getConsent(): 'all' | 'necessary' | null;
|
|
26
|
+
export function setConsent(level: 'all' | 'necessary'): void;
|
|
27
|
+
export function revokeConsent(): void;
|
|
28
|
+
export function isAnalyticsAllowed(): boolean;
|
|
29
|
+
export const TEXTS: { fr: Loi25Texts; en: Loi25Texts };
|
package/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rayels/loi25-core
|
|
3
|
+
* Loi 25 Quebec Cookie Consent Manager
|
|
4
|
+
* https://rayelsconsulting.com
|
|
5
|
+
*
|
|
6
|
+
* Lightweight cookie consent solution compliant with Quebec's Loi 25 (Bill 64).
|
|
7
|
+
* Bilingual (FR/EN), zero dependencies, ~2KB.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'loi25-consent';
|
|
11
|
+
const STORAGE_TIMESTAMP_KEY = 'loi25-consent-date';
|
|
12
|
+
|
|
13
|
+
const CONSENT_ALL = 'all';
|
|
14
|
+
const CONSENT_NECESSARY = 'necessary';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONFIG = {
|
|
17
|
+
lang: 'fr',
|
|
18
|
+
position: 'bottom', // 'bottom' | 'top'
|
|
19
|
+
theme: 'light', // 'light' | 'dark'
|
|
20
|
+
privacyPolicyUrl: '/politique-de-confidentialite',
|
|
21
|
+
poweredByLink: true,
|
|
22
|
+
expiryDays: 365,
|
|
23
|
+
onAcceptAll: null,
|
|
24
|
+
onAcceptNecessary: null,
|
|
25
|
+
onRevoke: null,
|
|
26
|
+
texts: null, // override default texts
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TEXTS = {
|
|
30
|
+
fr: {
|
|
31
|
+
title: 'Respect de votre vie privee',
|
|
32
|
+
message: 'Ce site utilise des cookies pour ameliorer votre experience. Conformement a la Loi 25 du Quebec, nous demandons votre consentement avant de collecter vos donnees.',
|
|
33
|
+
acceptAll: 'Tout accepter',
|
|
34
|
+
acceptNecessary: 'Necessaires seulement',
|
|
35
|
+
manage: 'Gerer mes preferences',
|
|
36
|
+
privacyPolicy: 'Politique de confidentialite',
|
|
37
|
+
poweredBy: 'Propulse par',
|
|
38
|
+
categories: {
|
|
39
|
+
necessary: { title: 'Necessaires', description: 'Essentiels au fonctionnement du site. Ne peuvent pas etre desactives.' },
|
|
40
|
+
analytics: { title: 'Analytiques', description: 'Nous aident a comprendre comment vous utilisez le site.' },
|
|
41
|
+
marketing: { title: 'Marketing', description: 'Utilises pour vous montrer des publicites pertinentes.' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
en: {
|
|
45
|
+
title: 'Your Privacy Matters',
|
|
46
|
+
message: 'This website uses cookies to improve your experience. In compliance with Quebec\'s Law 25, we ask for your consent before collecting your data.',
|
|
47
|
+
acceptAll: 'Accept All',
|
|
48
|
+
acceptNecessary: 'Necessary Only',
|
|
49
|
+
manage: 'Manage Preferences',
|
|
50
|
+
privacyPolicy: 'Privacy Policy',
|
|
51
|
+
poweredBy: 'Powered by',
|
|
52
|
+
categories: {
|
|
53
|
+
necessary: { title: 'Necessary', description: 'Essential for the website to function. Cannot be disabled.' },
|
|
54
|
+
analytics: { title: 'Analytics', description: 'Help us understand how you use the website.' },
|
|
55
|
+
marketing: { title: 'Marketing', description: 'Used to show you relevant advertisements.' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the current consent status
|
|
62
|
+
* @returns {'all' | 'necessary' | null}
|
|
63
|
+
*/
|
|
64
|
+
function getConsent() {
|
|
65
|
+
try {
|
|
66
|
+
var consent = localStorage.getItem(STORAGE_KEY);
|
|
67
|
+
if (!consent) return null;
|
|
68
|
+
|
|
69
|
+
var timestamp = localStorage.getItem(STORAGE_TIMESTAMP_KEY);
|
|
70
|
+
if (timestamp) {
|
|
71
|
+
var expiryMs = 365 * 24 * 60 * 60 * 1000;
|
|
72
|
+
if (Date.now() - parseInt(timestamp, 10) > expiryMs) {
|
|
73
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
74
|
+
localStorage.removeItem(STORAGE_TIMESTAMP_KEY);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return consent;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set consent
|
|
86
|
+
* @param {'all' | 'necessary'} level
|
|
87
|
+
*/
|
|
88
|
+
function setConsent(level) {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem(STORAGE_KEY, level);
|
|
91
|
+
localStorage.setItem(STORAGE_TIMESTAMP_KEY, String(Date.now()));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// localStorage not available
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Revoke consent
|
|
99
|
+
*/
|
|
100
|
+
function revokeConsent() {
|
|
101
|
+
try {
|
|
102
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
103
|
+
localStorage.removeItem(STORAGE_TIMESTAMP_KEY);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// localStorage not available
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if analytics/marketing cookies are allowed
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isAnalyticsAllowed() {
|
|
114
|
+
return getConsent() === CONSENT_ALL;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create and inject the consent banner into the DOM
|
|
119
|
+
* @param {object} userConfig
|
|
120
|
+
*/
|
|
121
|
+
function init(userConfig) {
|
|
122
|
+
var config = Object.assign({}, DEFAULT_CONFIG, userConfig);
|
|
123
|
+
var texts = config.texts || TEXTS[config.lang] || TEXTS.fr;
|
|
124
|
+
|
|
125
|
+
// Already consented? Don't show banner
|
|
126
|
+
if (getConsent()) return;
|
|
127
|
+
|
|
128
|
+
// Build banner HTML
|
|
129
|
+
var isDark = config.theme === 'dark';
|
|
130
|
+
var bgColor = isDark ? '#18181b' : '#ffffff';
|
|
131
|
+
var textColor = isDark ? '#e4e4e7' : '#1e293b';
|
|
132
|
+
var mutedColor = isDark ? '#a1a1aa' : '#64748b';
|
|
133
|
+
var borderColor = isDark ? '#27272a' : '#e2e8f0';
|
|
134
|
+
var btnPrimaryBg = '#1d4ed8';
|
|
135
|
+
var btnSecondaryBg = isDark ? '#27272a' : '#f1f5f9';
|
|
136
|
+
var btnSecondaryText = isDark ? '#e4e4e7' : '#334155';
|
|
137
|
+
|
|
138
|
+
var positionStyle = config.position === 'top'
|
|
139
|
+
? 'top:0;'
|
|
140
|
+
: 'bottom:0;';
|
|
141
|
+
|
|
142
|
+
var html = ''
|
|
143
|
+
+ '<div id="loi25-banner" style="'
|
|
144
|
+
+ 'position:fixed;left:0;right:0;' + positionStyle
|
|
145
|
+
+ 'z-index:999999;'
|
|
146
|
+
+ 'background:' + bgColor + ';'
|
|
147
|
+
+ 'border-' + (config.position === 'top' ? 'bottom' : 'top') + ':1px solid ' + borderColor + ';'
|
|
148
|
+
+ 'padding:20px 24px;'
|
|
149
|
+
+ 'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;'
|
|
150
|
+
+ 'font-size:14px;'
|
|
151
|
+
+ 'color:' + textColor + ';'
|
|
152
|
+
+ 'box-shadow:0 -2px 20px rgba(0,0,0,0.08);'
|
|
153
|
+
+ '">'
|
|
154
|
+
+ '<div style="max-width:960px;margin:0 auto;">'
|
|
155
|
+
+ '<div style="font-weight:700;font-size:16px;margin-bottom:8px;">' + texts.title + '</div>'
|
|
156
|
+
+ '<p style="margin:0 0 16px;color:' + mutedColor + ';line-height:1.5;">' + texts.message + '</p>'
|
|
157
|
+
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">'
|
|
158
|
+
+ '<button id="loi25-accept-all" style="'
|
|
159
|
+
+ 'background:' + btnPrimaryBg + ';color:#fff;border:none;padding:10px 20px;border-radius:8px;'
|
|
160
|
+
+ 'font-weight:600;font-size:14px;cursor:pointer;">'
|
|
161
|
+
+ texts.acceptAll + '</button>'
|
|
162
|
+
+ '<button id="loi25-accept-necessary" style="'
|
|
163
|
+
+ 'background:' + btnSecondaryBg + ';color:' + btnSecondaryText + ';border:1px solid ' + borderColor + ';'
|
|
164
|
+
+ 'padding:10px 20px;border-radius:8px;font-weight:600;font-size:14px;cursor:pointer;">'
|
|
165
|
+
+ texts.acceptNecessary + '</button>'
|
|
166
|
+
+ '<a href="' + config.privacyPolicyUrl + '" style="'
|
|
167
|
+
+ 'color:' + mutedColor + ';font-size:12px;text-decoration:underline;margin-left:8px;">'
|
|
168
|
+
+ texts.privacyPolicy + '</a>'
|
|
169
|
+
+ (config.poweredByLink
|
|
170
|
+
? '<a href="https://rayelsconsulting.com" target="_blank" rel="noopener" style="'
|
|
171
|
+
+ 'color:' + mutedColor + ';font-size:11px;margin-left:auto;text-decoration:none;opacity:0.7;">'
|
|
172
|
+
+ texts.poweredBy + ' Rayels</a>'
|
|
173
|
+
: '')
|
|
174
|
+
+ '</div>'
|
|
175
|
+
+ '</div>'
|
|
176
|
+
+ '</div>';
|
|
177
|
+
|
|
178
|
+
// Inject
|
|
179
|
+
var wrapper = document.createElement('div');
|
|
180
|
+
wrapper.innerHTML = html;
|
|
181
|
+
document.body.appendChild(wrapper.firstChild);
|
|
182
|
+
|
|
183
|
+
// Event listeners
|
|
184
|
+
document.getElementById('loi25-accept-all').addEventListener('click', function () {
|
|
185
|
+
setConsent(CONSENT_ALL);
|
|
186
|
+
removeBanner();
|
|
187
|
+
if (typeof config.onAcceptAll === 'function') config.onAcceptAll();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
document.getElementById('loi25-accept-necessary').addEventListener('click', function () {
|
|
191
|
+
setConsent(CONSENT_NECESSARY);
|
|
192
|
+
removeBanner();
|
|
193
|
+
if (typeof config.onAcceptNecessary === 'function') config.onAcceptNecessary();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function removeBanner() {
|
|
198
|
+
var banner = document.getElementById('loi25-banner');
|
|
199
|
+
if (banner) banner.remove();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Export for different module systems
|
|
203
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
204
|
+
module.exports = { init: init, getConsent: getConsent, setConsent: setConsent, revokeConsent: revokeConsent, isAnalyticsAllowed: isAnalyticsAllowed, TEXTS: TEXTS };
|
|
205
|
+
}
|
|
206
|
+
if (typeof window !== 'undefined') {
|
|
207
|
+
window.Loi25 = { init: init, getConsent: getConsent, setConsent: setConsent, revokeConsent: revokeConsent, isAnalyticsAllowed: isAnalyticsAllowed, TEXTS: TEXTS };
|
|
208
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rayels/loi25-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Loi 25 Quebec cookie consent manager — lightweight, bilingual, GDPR/Loi 25 compliant. By Rayels Consulting.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"loi 25",
|
|
9
|
+
"loi25",
|
|
10
|
+
"quebec",
|
|
11
|
+
"cookie consent",
|
|
12
|
+
"privacy",
|
|
13
|
+
"gdpr",
|
|
14
|
+
"cookie banner",
|
|
15
|
+
"conformite",
|
|
16
|
+
"cookies",
|
|
17
|
+
"consent manager",
|
|
18
|
+
"quebec privacy law",
|
|
19
|
+
"bill 64"
|
|
20
|
+
],
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Rayels Consulting",
|
|
23
|
+
"email": "contact@rayelsconsulting.com",
|
|
24
|
+
"url": "https://rayelsconsulting.com"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://rayelsconsulting.com",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/raracx/rayels-loi25-quebec"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|