astro-consent 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/LICENSE.md ADDED
@@ -0,0 +1,36 @@
1
+ BSD 4-Clause License
2
+
3
+ Copyright (c) 2026, Velohost
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. All advertising materials mentioning features or use of this software
17
+ must display the following acknowledgement:
18
+
19
+ This product includes software developed by Velohost
20
+ (https://velohost.co.uk)
21
+
22
+ 4. Neither the name of Velohost nor the names of its contributors may be used
23
+ to endorse or promote products derived from this software without
24
+ specific prior written permission.
25
+
26
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
30
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36
+ POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # astro-cookiebanner
2
+
3
+ A **privacy‑first, zero‑dependency cookie consent banner** for Astro projects — built for speed, compliance, and full visual control.
4
+
5
+ Designed and maintained by **Velohost**.
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
11
+ - ✅ GDPR / UK GDPR friendly
12
+ - 🍪 Essential, Analytics & Marketing categories
13
+ - 🎛️ Manage preferences modal with toggle switches
14
+ - ⚡ No external dependencies
15
+ - 🎨 Fully themeable via CSS variables
16
+ - 🧠 Frontend‑controlled script loading
17
+ - 🧩 Astro Integration + CLI installer
18
+ - 🔁 Easy uninstall via CLI
19
+ - 🌍 Framework‑agnostic frontend API
20
+
21
+ ---
22
+
23
+ ## 📦 Installation
24
+
25
+ ```bash
26
+ npm install astro-cookiebanner
27
+ ```
28
+
29
+ Then run the installer inside your Astro project:
30
+
31
+ ```bash
32
+ npx astro-cookiebanner
33
+ ```
34
+
35
+ To remove everything:
36
+
37
+ ```bash
38
+ npx astro-cookiebanner remove
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 🔧 Usage
44
+
45
+ ```ts
46
+ import astroCookieBanner from "astro-cookiebanner";
47
+
48
+ export default {
49
+ integrations: [
50
+ astroCookieBanner({
51
+ siteName: "My Website",
52
+ policyUrl: "/privacy",
53
+ consent: {
54
+ days: 30,
55
+ storageKey: "astro-cookie-consent"
56
+ },
57
+ categories: {
58
+ analytics: false,
59
+ marketing: false
60
+ }
61
+ })
62
+ ]
63
+ };
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 🧠 Frontend API
69
+
70
+ ```js
71
+ window.cookieConsent.get();
72
+ window.cookieConsent.set({ essential: true, analytics: true });
73
+ window.cookieConsent.reset();
74
+ ```
75
+
76
+ Example conditional loading:
77
+
78
+ ```js
79
+ const consent = window.cookieConsent.get();
80
+
81
+ if (consent?.categories?.analytics) {
82
+ // Load analytics script
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 🎨 Theming
89
+
90
+ All visuals are controlled via:
91
+
92
+ ```
93
+ src/cookiebanner.css
94
+ ```
95
+
96
+ This file is never overwritten.
97
+
98
+ ---
99
+
100
+ ## 🔐 Privacy
101
+
102
+ - No cookies before consent
103
+ - No tracking without permission
104
+ - No external calls
105
+ - Stored locally with TTL
106
+
107
+ ---
108
+
109
+ ## 🏷️ License & Attribution
110
+
111
+ Open‑source with **mandatory attribution**.
112
+
113
+ Any public use, fork, or redistribution **must credit Velohost**.
114
+
115
+ See `LICENSE.md` for full terms.
116
+
117
+ ---
118
+
119
+ ## 🏢 Velohost
120
+
121
+ Built by **Velohost**
122
+ https://velohost.co.uk
123
+
124
+ ---
125
+
126
+ ## 🤝 Contributions
127
+
128
+ PRs welcome — attribution must be preserved.
package/dist/cli.cjs ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const process = __importStar(require("node:process"));
40
+ const CWD = process.cwd();
41
+ const args = process.argv.slice(2);
42
+ const command = args[0] ?? "install";
43
+ const ASTRO_CONFIG_FILES = [
44
+ "astro.config.mjs",
45
+ "astro.config.ts",
46
+ "astro.config.js"
47
+ ];
48
+ function findAstroConfig() {
49
+ for (const file of ASTRO_CONFIG_FILES) {
50
+ const fullPath = path.join(CWD, file);
51
+ if (fs.existsSync(fullPath))
52
+ return fullPath;
53
+ }
54
+ return null;
55
+ }
56
+ function exitWith(message, code = 1) {
57
+ console.error(`\n❌ ${message}\n`);
58
+ process.exit(code);
59
+ }
60
+ /* ─────────────────────────────────────
61
+ Locate astro.config
62
+ ───────────────────────────────────── */
63
+ const configPath = findAstroConfig();
64
+ if (!configPath) {
65
+ exitWith("No astro.config.(mjs|ts|js) found. Run this in the root of an Astro project.");
66
+ }
67
+ let source = fs.readFileSync(configPath, "utf8");
68
+ /* ─────────────────────────────────────
69
+ REMOVE MODE
70
+ ───────────────────────────────────── */
71
+ if (command === "remove") {
72
+ source = source.replace(/\s*astroCookieBanner\s*\(\s*\{[\s\S]*?\}\s*\),?/gm, "");
73
+ source = source.replace(/import\s+astroCookieBanner\s+from\s+["']astro-cookiebanner["'];?\n?/, "");
74
+ fs.writeFileSync(configPath, source.trim() + "\n", "utf8");
75
+ const cssFile = path.join(CWD, "src", "cookiebanner.css");
76
+ if (fs.existsSync(cssFile))
77
+ fs.unlinkSync(cssFile);
78
+ console.log("\n🧹 astro-cookiebanner fully removed\n");
79
+ process.exit(0);
80
+ }
81
+ /* ─────────────────────────────────────
82
+ INSTALL MODE
83
+ ───────────────────────────────────── */
84
+ const cssDir = path.join(CWD, "src");
85
+ const cssFile = path.join(cssDir, "cookiebanner.css");
86
+ if (!fs.existsSync(cssDir)) {
87
+ fs.mkdirSync(cssDir, { recursive: true });
88
+ }
89
+ /* ─────────────────────────────────────
90
+ Create FULL CSS VARIABLE OVERRIDE
91
+ ───────────────────────────────────── */
92
+ if (!fs.existsSync(cssFile)) {
93
+ fs.writeFileSync(cssFile, `/* =========================================================
94
+ astro-cookiebanner — FULL THEME VARIABLES
95
+ All visuals are controlled from here.
96
+ This file is NEVER overwritten.
97
+ ========================================================= */
98
+
99
+ :root {
100
+ /* ───── Core ───── */
101
+ --cb-font: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
102
+
103
+ /* ───── Banner ───── */
104
+ --cb-bg: rgba(12,18,32,0.88);
105
+ --cb-border: rgba(255,255,255,0.08);
106
+ --cb-text: #e5e7eb;
107
+ --cb-muted: #9ca3af;
108
+ --cb-link: #60a5fa;
109
+ --cb-radius: 16px;
110
+ --cb-shadow: 0 20px 40px rgba(0,0,0,0.35);
111
+
112
+ /* ───── Buttons ───── */
113
+ --cb-btn-radius: 999px;
114
+ --cb-btn-padding: 10px 18px;
115
+
116
+ --cb-accept-bg: #22c55e;
117
+ --cb-accept-text: #052e16;
118
+
119
+ --cb-reject-bg: #374151;
120
+ --cb-reject-text: #e5e7eb;
121
+
122
+ --cb-manage-bg: transparent;
123
+ --cb-manage-text: #e5e7eb;
124
+ --cb-manage-border: rgba(255,255,255,0.15);
125
+
126
+ /* ───── Modal ───── */
127
+ --cb-modal-backdrop: rgba(0,0,0,0.55);
128
+ --cb-modal-bg: #0c1220;
129
+ --cb-modal-radius: 18px;
130
+ --cb-modal-width: 480px;
131
+
132
+ /* ───── Toggles ───── */
133
+ --cb-toggle-off-bg: #374151;
134
+ --cb-toggle-on-bg: #22c55e;
135
+ --cb-toggle-knob: #ffffff;
136
+ }
137
+
138
+ /* =========================================================
139
+ Banner
140
+ ========================================================= */
141
+
142
+ #astro-cookie-banner {
143
+ position: fixed;
144
+ left: 16px;
145
+ right: 16px;
146
+ bottom: 16px;
147
+ z-index: 9999;
148
+ font-family: var(--cb-font);
149
+ }
150
+
151
+ .cb-container {
152
+ max-width: 1200px;
153
+ margin: 0 auto;
154
+ padding: 20px 24px;
155
+ display: flex;
156
+ gap: 24px;
157
+ justify-content: space-between;
158
+ align-items: center;
159
+
160
+ background: var(--cb-bg);
161
+ backdrop-filter: blur(14px);
162
+ border-radius: var(--cb-radius);
163
+ border: 1px solid var(--cb-border);
164
+ box-shadow: var(--cb-shadow);
165
+ color: var(--cb-text);
166
+ }
167
+
168
+ .cb-title {
169
+ font-size: 16px;
170
+ font-weight: 600;
171
+ }
172
+
173
+ .cb-desc {
174
+ font-size: 14px;
175
+ color: var(--cb-muted);
176
+ }
177
+
178
+ .cb-desc a {
179
+ color: var(--cb-link);
180
+ text-decoration: none;
181
+ }
182
+
183
+ .cb-actions {
184
+ display: flex;
185
+ gap: 10px;
186
+ }
187
+
188
+ /* =========================================================
189
+ Buttons
190
+ ========================================================= */
191
+
192
+ .cb-actions button {
193
+ padding: var(--cb-btn-padding);
194
+ border-radius: var(--cb-btn-radius);
195
+ border: 0;
196
+ font-size: 14px;
197
+ font-weight: 600;
198
+ cursor: pointer;
199
+ }
200
+
201
+ .cb-accept {
202
+ background: var(--cb-accept-bg);
203
+ color: var(--cb-accept-text);
204
+ }
205
+
206
+ .cb-reject {
207
+ background: var(--cb-reject-bg);
208
+ color: var(--cb-reject-text);
209
+ }
210
+
211
+ .cb-manage {
212
+ background: var(--cb-manage-bg);
213
+ color: var(--cb-manage-text);
214
+ border: 1px solid var(--cb-manage-border);
215
+ }
216
+
217
+ /* =========================================================
218
+ Modal
219
+ ========================================================= */
220
+
221
+ #astro-cookie-modal {
222
+ position: fixed;
223
+ inset: 0;
224
+ background: var(--cb-modal-backdrop);
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ z-index: 10000;
229
+ }
230
+
231
+ .cb-modal {
232
+ width: 100%;
233
+ max-width: var(--cb-modal-width);
234
+ background: var(--cb-modal-bg);
235
+ border-radius: var(--cb-modal-radius);
236
+ padding: 24px;
237
+ color: var(--cb-text);
238
+ }
239
+
240
+ /* =========================================================
241
+ Toggles
242
+ ========================================================= */
243
+
244
+ .cb-toggle {
245
+ width: 44px;
246
+ height: 24px;
247
+ background: var(--cb-toggle-off-bg);
248
+ border-radius: 999px;
249
+ position: relative;
250
+ cursor: pointer;
251
+ }
252
+
253
+ .cb-toggle span {
254
+ position: absolute;
255
+ width: 18px;
256
+ height: 18px;
257
+ background: var(--cb-toggle-knob);
258
+ border-radius: 50%;
259
+ top: 3px;
260
+ left: 3px;
261
+ transition: transform 0.2s ease;
262
+ }
263
+
264
+ .cb-toggle.active {
265
+ background: var(--cb-toggle-on-bg);
266
+ }
267
+
268
+ .cb-toggle.active span {
269
+ transform: translateX(20px);
270
+ }
271
+
272
+ /* =========================================================
273
+ Mobile
274
+ ========================================================= */
275
+
276
+ @media (max-width: 640px) {
277
+ .cb-container {
278
+ flex-direction: column;
279
+ align-items: stretch;
280
+ gap: 16px;
281
+ }
282
+ }
283
+ `, "utf8");
284
+ console.log("🎨 Created src/cookiebanner.css (CSS variables enabled)");
285
+ }
286
+ /* ─────────────────────────────────────
287
+ Inject Astro integration
288
+ ───────────────────────────────────── */
289
+ if (!source.includes(`from "astro-cookiebanner"`)) {
290
+ source = `import astroCookieBanner from "astro-cookiebanner";\n${source}`;
291
+ }
292
+ if (!source.includes("astroCookieBanner(")) {
293
+ const injection = ` astroCookieBanner({
294
+ siteName: "My Website",
295
+ policyUrl: "/privacy",
296
+ consent: {
297
+ days: 30,
298
+ storageKey: "astro-cookie-consent"
299
+ },
300
+ categories: {
301
+ analytics: false,
302
+ marketing: false
303
+ }
304
+ }),
305
+ `;
306
+ source = source.replace(/integrations\s*:\s*\[/, match => `${match}\n${injection}`);
307
+ }
308
+ fs.writeFileSync(configPath, source, "utf8");
309
+ console.log("\n🎉 astro-cookiebanner installed successfully");
310
+ console.log("👉 Edit src/cookiebanner.css to theme everything");
311
+ console.log("👉 Run `astro-cookiebanner remove` to uninstall\n");
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Default configuration values.
3
+ * These are ONLY used as fallbacks if the user omits something.
4
+ * User config always takes priority.
5
+ */
6
+ export const DEFAULT_CONFIG = {
7
+ siteName: "This website",
8
+ policyUrl: "/privacy",
9
+ consent: {
10
+ enabled: true,
11
+ days: 30,
12
+ storageKey: "astro-cookie-consent"
13
+ },
14
+ categories: {
15
+ essential: {
16
+ label: "Essential",
17
+ description: "Required for the website to function correctly",
18
+ enabled: true,
19
+ readonly: true
20
+ },
21
+ analytics: {
22
+ label: "Analytics",
23
+ description: "Helps us understand how visitors use the site",
24
+ enabled: false
25
+ },
26
+ marketing: {
27
+ label: "Marketing",
28
+ description: "Used to deliver personalised ads",
29
+ enabled: false
30
+ }
31
+ }
32
+ };
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { DEFAULT_CONFIG } from "./defaults.js";
5
+ /**
6
+ * Safely loads the user config file and merges it with defaults.
7
+ * User values always override defaults.
8
+ * Cache-busted to ensure updates are picked up during dev.
9
+ */
10
+ export async function loadUserConfig(projectRoot) {
11
+ const configPath = path.join(projectRoot, "src", "cookiebanner", "config.ts");
12
+ let userConfig = {};
13
+ try {
14
+ // 🔑 IMPORTANT:
15
+ // Bust Node ESM import cache using file modified time
16
+ const stat = fs.statSync(configPath);
17
+ const cacheBuster = `?v=${stat.mtimeMs}`;
18
+ const imported = await import(
19
+ /* @vite-ignore */
20
+ pathToFileURL(configPath).href + cacheBuster);
21
+ userConfig = imported?.default ?? {};
22
+ }
23
+ catch (err) {
24
+ console.warn("[cookiebanner] Failed to load user config, falling back to defaults:", err);
25
+ }
26
+ return {
27
+ /* ─────────────────────────────
28
+ Site name
29
+ ───────────────────────────── */
30
+ siteName: userConfig.siteName ?? DEFAULT_CONFIG.siteName,
31
+ /* ─────────────────────────────
32
+ Policy URL
33
+ ───────────────────────────── */
34
+ policyUrl: userConfig.policyUrl ?? DEFAULT_CONFIG.policyUrl,
35
+ /* ─────────────────────────────
36
+ Consent settings
37
+ ───────────────────────────── */
38
+ consent: {
39
+ enabled: userConfig.consent?.enabled ??
40
+ DEFAULT_CONFIG.consent.enabled,
41
+ days: userConfig.consent?.days ??
42
+ DEFAULT_CONFIG.consent.days,
43
+ storageKey: userConfig.consent?.storageKey ??
44
+ DEFAULT_CONFIG.consent.storageKey
45
+ },
46
+ /* ─────────────────────────────
47
+ Categories
48
+ ───────────────────────────── */
49
+ categories: mergeCategories(userConfig.categories, DEFAULT_CONFIG.categories)
50
+ };
51
+ }
52
+ /**
53
+ * Merge category config safely.
54
+ * Defaults are preserved, user overrides where provided.
55
+ */
56
+ function mergeCategories(userCategories, defaultCategories) {
57
+ const merged = {};
58
+ // Start with defaults
59
+ for (const key of Object.keys(defaultCategories)) {
60
+ merged[key] = {
61
+ ...defaultCategories[key],
62
+ ...(userCategories?.[key] ?? {})
63
+ };
64
+ }
65
+ // Include any custom categories the user added
66
+ if (userCategories) {
67
+ for (const key of Object.keys(userCategories)) {
68
+ if (!merged[key]) {
69
+ merged[key] = userCategories[key];
70
+ }
71
+ }
72
+ }
73
+ return merged;
74
+ }
package/dist/index.js ADDED
@@ -0,0 +1,333 @@
1
+ export default function astroCookieBanner(options = {}) {
2
+ const siteName = options.siteName ?? "This website";
3
+ const policyUrl = options.policyUrl ?? "/privacy";
4
+ const consentDays = options.consent?.days ?? 30;
5
+ const storageKey = options.consent?.storageKey ?? "astro-cookie-consent";
6
+ const defaultCategories = {
7
+ essential: true,
8
+ analytics: false,
9
+ marketing: false,
10
+ ...options.categories
11
+ };
12
+ const ttl = consentDays * 24 * 60 * 60 * 1000;
13
+ return {
14
+ name: "astro-cookiebanner",
15
+ hooks: {
16
+ "astro:config:setup": ({ injectScript }) => {
17
+ /* ─────────────────────────────────────
18
+ ALL STYLES (banner + modal)
19
+ ───────────────────────────────────── */
20
+ injectScript("head-inline", `
21
+ const style = document.createElement("style");
22
+ style.innerHTML = \`
23
+ :root {
24
+ --cb-bg: rgba(12,18,32,.88);
25
+ --cb-border: rgba(255,255,255,.08);
26
+ --cb-text: #e5e7eb;
27
+ --cb-muted: #9ca3af;
28
+ --cb-link: #60a5fa;
29
+ --cb-accept: #22c55e;
30
+ --cb-reject: #374151;
31
+ }
32
+
33
+ /* ───── Banner ───── */
34
+
35
+ #astro-cookie-banner {
36
+ position: fixed;
37
+ left: 16px;
38
+ right: 16px;
39
+ bottom: 16px;
40
+ z-index: 9999;
41
+ font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
42
+ }
43
+
44
+ .cb-container {
45
+ max-width: 1200px;
46
+ margin: 0 auto;
47
+ padding: 20px 24px;
48
+ display: flex;
49
+ gap: 24px;
50
+ justify-content: space-between;
51
+ align-items: center;
52
+
53
+ background: var(--cb-bg);
54
+ backdrop-filter: blur(14px);
55
+ border-radius: 16px;
56
+ border: 1px solid var(--cb-border);
57
+ box-shadow: 0 20px 40px rgba(0,0,0,.35);
58
+
59
+ color: var(--cb-text);
60
+ }
61
+
62
+ .cb-text {
63
+ max-width: 760px;
64
+ }
65
+
66
+ .cb-title {
67
+ font-size: 16px;
68
+ font-weight: 600;
69
+ }
70
+
71
+ .cb-desc {
72
+ font-size: 14px;
73
+ color: var(--cb-muted);
74
+ }
75
+
76
+ .cb-desc a {
77
+ color: var(--cb-link);
78
+ text-decoration: none;
79
+ }
80
+
81
+ .cb-desc a:hover {
82
+ text-decoration: underline;
83
+ }
84
+
85
+ .cb-actions {
86
+ display: flex;
87
+ gap: 10px;
88
+ flex-shrink: 0;
89
+ }
90
+
91
+ .cb-actions button {
92
+ padding: 10px 18px;
93
+ border-radius: 999px;
94
+ border: 0;
95
+ font-size: 14px;
96
+ font-weight: 600;
97
+ cursor: pointer;
98
+ }
99
+
100
+ .cb-accept {
101
+ background: var(--cb-accept);
102
+ color: #052e16;
103
+ }
104
+
105
+ .cb-reject {
106
+ background: var(--cb-reject);
107
+ color: #e5e7eb;
108
+ }
109
+
110
+ .cb-manage {
111
+ background: transparent;
112
+ color: var(--cb-text);
113
+ border: 1px solid var(--cb-border);
114
+ }
115
+
116
+ /* ───── Modal ───── */
117
+
118
+ #astro-cookie-modal {
119
+ position: fixed;
120
+ inset: 0;
121
+ background: rgba(0,0,0,.55);
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ z-index: 10000;
126
+ font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
127
+ }
128
+
129
+ .cb-modal {
130
+ width: 100%;
131
+ max-width: 480px;
132
+ background: #0c1220;
133
+ border-radius: 18px;
134
+ padding: 24px;
135
+ border: 1px solid var(--cb-border);
136
+ color: var(--cb-text);
137
+ }
138
+
139
+ .cb-modal h3 {
140
+ margin: 0 0 16px;
141
+ }
142
+
143
+ .cb-row {
144
+ display: flex;
145
+ justify-content: space-between;
146
+ align-items: center;
147
+ padding: 12px 0;
148
+ border-bottom: 1px solid var(--cb-border);
149
+ }
150
+
151
+ .cb-row:last-child {
152
+ border-bottom: 0;
153
+ }
154
+
155
+ .cb-toggle {
156
+ width: 44px;
157
+ height: 24px;
158
+ background: #374151;
159
+ border-radius: 999px;
160
+ position: relative;
161
+ cursor: pointer;
162
+ }
163
+
164
+ .cb-toggle span {
165
+ position: absolute;
166
+ width: 18px;
167
+ height: 18px;
168
+ background: #fff;
169
+ border-radius: 50%;
170
+ top: 3px;
171
+ left: 3px;
172
+ transition: transform .2s;
173
+ }
174
+
175
+ .cb-toggle.active {
176
+ background: var(--cb-accept);
177
+ }
178
+
179
+ .cb-toggle.active span {
180
+ transform: translateX(20px);
181
+ }
182
+
183
+ @media (max-width: 640px) {
184
+ .cb-container {
185
+ flex-direction: column;
186
+ align-items: stretch;
187
+ gap: 16px;
188
+ }
189
+ }
190
+ \`;
191
+ document.head.appendChild(style);
192
+ `);
193
+ /* ─────────────────────────────────────
194
+ Consent runtime
195
+ ───────────────────────────────────── */
196
+ injectScript("page", `
197
+ (() => {
198
+ const KEY = "${storageKey}";
199
+ const TTL = ${ttl};
200
+
201
+ function now(){ return Date.now(); }
202
+
203
+ function read(){
204
+ try{
205
+ const raw = localStorage.getItem(KEY);
206
+ if(!raw) return null;
207
+ const data = JSON.parse(raw);
208
+ if(data.expiresAt < now()){
209
+ localStorage.removeItem(KEY);
210
+ return null;
211
+ }
212
+ return data;
213
+ }catch{ return null; }
214
+ }
215
+
216
+ function write(categories){
217
+ localStorage.setItem(KEY, JSON.stringify({
218
+ updatedAt: now(),
219
+ expiresAt: now() + TTL,
220
+ categories
221
+ }));
222
+ }
223
+
224
+ window.cookieConsent = {
225
+ get: read,
226
+ set: write,
227
+ reset(){
228
+ localStorage.removeItem(KEY);
229
+ location.reload();
230
+ }
231
+ };
232
+ })();
233
+ `);
234
+ /* ─────────────────────────────────────
235
+ Banner + modal UI
236
+ ───────────────────────────────────── */
237
+ injectScript("page", `
238
+ (() => {
239
+ if (window.cookieConsent.get()) return;
240
+
241
+ const state = { ...${JSON.stringify(defaultCategories)} };
242
+
243
+ const banner = document.createElement("div");
244
+ banner.id = "astro-cookie-banner";
245
+
246
+ banner.innerHTML = \`
247
+ <div class="cb-container">
248
+ <div class="cb-text">
249
+ <div class="cb-title">${siteName} uses cookies</div>
250
+ <div class="cb-desc">
251
+ Choose how your data is used.
252
+ <a href="${policyUrl}">Learn more</a>
253
+ </div>
254
+ </div>
255
+ <div class="cb-actions">
256
+ <button class="cb-manage">Manage</button>
257
+ <button class="cb-reject">Reject</button>
258
+ <button class="cb-accept">Accept all</button>
259
+ </div>
260
+ </div>
261
+ \`;
262
+
263
+ document.body.appendChild(banner);
264
+
265
+ banner.querySelector(".cb-accept").onclick = () => {
266
+ window.cookieConsent.set({ essential:true, analytics:true, marketing:true });
267
+ banner.remove();
268
+ };
269
+
270
+ banner.querySelector(".cb-reject").onclick = () => {
271
+ window.cookieConsent.set({ essential:true });
272
+ banner.remove();
273
+ };
274
+
275
+ banner.querySelector(".cb-manage").onclick = openModal;
276
+
277
+ function openModal(){
278
+ const modal = document.createElement("div");
279
+ modal.id = "astro-cookie-modal";
280
+
281
+ modal.innerHTML = \`
282
+ <div class="cb-modal">
283
+ <h3>Cookie preferences</h3>
284
+
285
+ <div class="cb-row">
286
+ <span>Essential</span>
287
+ <strong>Always on</strong>
288
+ </div>
289
+
290
+ <div class="cb-row">
291
+ <span>Analytics</span>
292
+ <div class="cb-toggle" data-key="analytics"><span></span></div>
293
+ </div>
294
+
295
+ <div class="cb-row">
296
+ <span>Marketing</span>
297
+ <div class="cb-toggle" data-key="marketing"><span></span></div>
298
+ </div>
299
+
300
+ <div class="cb-actions" style="margin-top:16px;justify-content:flex-end">
301
+ <button class="cb-accept">Save preferences</button>
302
+ </div>
303
+ </div>
304
+ \`;
305
+
306
+ document.body.appendChild(modal);
307
+
308
+ modal.querySelectorAll(".cb-toggle").forEach(toggle => {
309
+ const key = toggle.getAttribute("data-key");
310
+ if(state[key]) toggle.classList.add("active");
311
+
312
+ toggle.onclick = () => {
313
+ state[key] = !state[key];
314
+ toggle.classList.toggle("active");
315
+ };
316
+ });
317
+
318
+ modal.querySelector(".cb-accept").onclick = () => {
319
+ window.cookieConsent.set({ essential:true, ...state });
320
+ modal.remove();
321
+ banner.remove();
322
+ };
323
+
324
+ modal.onclick = e => {
325
+ if(e.target === modal) modal.remove();
326
+ };
327
+ }
328
+ })();
329
+ `);
330
+ }
331
+ }
332
+ };
333
+ }
@@ -0,0 +1,117 @@
1
+ export const DEFAULT_CSS = `
2
+ :root {
3
+ /* Layout */
4
+ --cb-z-index: 9999;
5
+ --cb-max-width: 960px;
6
+ --cb-padding: 16px;
7
+ --cb-gap: 12px;
8
+ --cb-radius: 10px;
9
+
10
+ /* Colours */
11
+ --cb-bg: #111827;
12
+ --cb-surface: #1f2933;
13
+ --cb-text: #ffffff;
14
+ --cb-muted: #9ca3af;
15
+ --cb-border: #374151;
16
+ --cb-accent: #6366f1;
17
+
18
+ /* Buttons */
19
+ --cb-btn-bg: var(--cb-accent);
20
+ --cb-btn-text: #ffffff;
21
+ --cb-btn-secondary-bg: transparent;
22
+ --cb-btn-secondary-text: var(--cb-text);
23
+ --cb-btn-border: var(--cb-border);
24
+
25
+ /* Typography */
26
+ --cb-font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
27
+ --cb-title-size: 1rem;
28
+ --cb-text-size: 0.875rem;
29
+ }
30
+
31
+ #astro-cookie-banner {
32
+ position: fixed;
33
+ bottom: 0;
34
+ left: 0;
35
+ right: 0;
36
+ z-index: var(--cb-z-index);
37
+ background: var(--cb-bg);
38
+ color: var(--cb-text);
39
+ font-family: var(--cb-font-family);
40
+ border-top: 1px solid var(--cb-border);
41
+ }
42
+
43
+ #astro-cookie-banner > * {
44
+ max-width: var(--cb-max-width);
45
+ margin: 0 auto;
46
+ padding: var(--cb-padding);
47
+ }
48
+
49
+ #astro-cookie-banner h2 {
50
+ margin: 0 0 4px;
51
+ font-size: var(--cb-title-size);
52
+ }
53
+
54
+ #astro-cookie-banner p {
55
+ margin: 0;
56
+ font-size: var(--cb-text-size);
57
+ color: var(--cb-muted);
58
+ }
59
+
60
+ #astro-cookie-banner a {
61
+ color: var(--cb-accent);
62
+ text-decoration: underline;
63
+ }
64
+
65
+ .astro-cookie-categories {
66
+ display: grid;
67
+ gap: var(--cb-gap);
68
+ margin-top: var(--cb-gap);
69
+ }
70
+
71
+ .astro-cookie-category {
72
+ background: var(--cb-surface);
73
+ padding: 12px;
74
+ border-radius: var(--cb-radius);
75
+ border: 1px solid var(--cb-border);
76
+ }
77
+
78
+ .astro-cookie-label {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 8px;
82
+ font-weight: 600;
83
+ }
84
+
85
+ .astro-cookie-description {
86
+ margin: 4px 0 0 26px;
87
+ font-size: 0.8rem;
88
+ color: var(--cb-muted);
89
+ }
90
+
91
+ .astro-cookie-actions {
92
+ display: flex;
93
+ gap: 8px;
94
+ justify-content: flex-end;
95
+ margin-top: var(--cb-gap);
96
+ flex-wrap: wrap;
97
+ }
98
+
99
+ .astro-cookie-actions button {
100
+ padding: 8px 14px;
101
+ border-radius: var(--cb-radius);
102
+ font-size: 0.875rem;
103
+ cursor: pointer;
104
+ border: 1px solid var(--cb-btn-border);
105
+ }
106
+
107
+ .astro-cookie-actions button:first-child {
108
+ background: var(--cb-btn-secondary-bg);
109
+ color: var(--cb-btn-secondary-text);
110
+ }
111
+
112
+ .astro-cookie-actions button:last-child {
113
+ background: var(--cb-btn-bg);
114
+ color: var(--cb-btn-text);
115
+ border-color: var(--cb-btn-bg);
116
+ }
117
+ `;
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "astro-consent",
3
+ "version": "1.0.0",
4
+ "description": "A privacy-first, GDPR-compliant cookie consent banner for Astro with a built-in preferences modal, zero dependencies, and full theme control.",
5
+ "type": "module",
6
+ "author": {
7
+ "name": "Velohost",
8
+ "url": "https://velohost.co.uk"
9
+ },
10
+ "license": "BSD-4-Clause",
11
+ "homepage": "https://velohost.co.uk",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/velohost/astro-cookiebanner.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/velohost/astro-cookiebanner/issues"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "exports": {
21
+ ".": "./dist/index.js"
22
+ },
23
+ "bin": {
24
+ "astro-cookiebanner": "dist/cli.cjs"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.cli.json"
33
+ },
34
+ "keywords": [
35
+ "astro",
36
+ "astro-integration",
37
+ "cookie-banner",
38
+ "cookie-consent",
39
+ "gdpr",
40
+ "privacy",
41
+ "analytics-consent",
42
+ "marketing-consent",
43
+ "withastro",
44
+ "velohost"
45
+ ],
46
+ "peerDependencies": {
47
+ "astro": "^4.0.0 || ^5.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "astro": "^5.16.6"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ }
55
+ }