feat: Upgrade to Astro with full PDPA compliance

PDPA Features:
 Cookie consent banner
 Consent logging API
 Admin dashboard
 Privacy Policy
 Terms & Conditions

Technical:
 Astro 5.x + Tailwind v4
 Docker on port 80
 SQLite database
 15 pages built

Ready for Easypanel deployment.
This commit is contained in:
Kunthawat
2026-03-12 10:01:04 +07:00
parent 668f69048f
commit 77ac4d2d05
13719 changed files with 307487 additions and 25765 deletions

21
node_modules/astro-consent/LICENSE.md generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Velohost
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

200
node_modules/astro-consent/README.md generated vendored Normal file
View File

@@ -0,0 +1,200 @@
# astro-consent
A **privacy-first, zero-dependency cookie consent banner** for Astro projects — built for speed, compliance, and total visual control.
Designed and maintained by **Velohost**.
---
## Why astro-consent?
Most cookie consent solutions are bloated, opaque, or tied to third-party services.
**astro-consent** is built differently:
- No trackers
- No remote calls
- No analytics SDKs
- No vendor lock-in
- No runtime dependencies
Just a **fast, deterministic, developer-controlled consent layer** that respects user privacy and legal boundaries.
---
## ✨ Features
- ✅ GDPR / UK GDPR friendly
- 🍪 Consent categories: Essential, Analytics, Marketing
- 🎛️ Preferences modal with toggle switches
- ⚡ Zero runtime dependencies
- 🎨 Fully themeable via CSS variables
- 🧠 Frontend-controlled script loading
- 🧩 Native Astro integration
- 🧾 Built-in TypeScript declarations
- 🛠️ CLI installer & remover
- 🔁 Clean uninstall with no residue
- 🌍 Framework-agnostic frontend API
---
## 📦 Installation (Required)
This package uses **both an Astro integration and a CLI installer**.
### 1⃣ Install the package
```bash
npm install astro-consent
```
This step is **required** so Astro can import the integration at build time.
---
### 2⃣ Run the installer
```bash
npx astro-consent
```
This will automatically:
- Inject the Astro integration into `astro.config.*`
- Create `src/cookiebanner.css` for theme control
- Enable the consent banner across your site
No manual wiring required.
---
### ❌ Uninstall
```bash
npx astro-consent remove
```
This cleanly removes:
- The integration entry
- Generated files
- All banner logic
No orphaned config. No hidden state.
---
## 🔧 Astro Integration Usage
```ts
import astroConsent from "astro-consent";
export default {
integrations: [
astroConsent({
siteName: "My Website",
policyUrl: "https://example.com/privacy-policy",
consent: {
days: 30,
storageKey: "astro-cookie-consent"
},
categories: {
analytics: false,
marketing: false
}
})
]
};
```
### Configuration notes
- **policyUrl**
A full, public URL to your Privacy or Cookie Policy page.
This is linked directly from the consent banner.
- **consent.days**
How long (in days) consent is stored before the user is asked again.
- **consent.storageKey**
The `localStorage` key used to persist consent.
You may change this freely if you need per-site or per-environment isolation.
- **categories.analytics**
Allows analytics scripts to load only after consent.
Typical use: Plausible, self-hosted analytics, Google Analytics.
- **categories.marketing**
Allows marketing and advertising scripts to load only after consent.
Typical use: ad pixels, remarketing tags, embedded social trackers.
Scripts outside the **essential** category must be conditionally loaded.
---
## 🧠 Frontend API
```js
window.cookieConsent.get();
window.cookieConsent.set({ essential: true, analytics: true });
window.cookieConsent.reset();
```
## 🧾 TypeScript
TypeScript declarations are included in the package.
No separate `@types` install is required.
---
## 🎨 Theming
All visuals are controlled via:
```
src/cookiebanner.css
```
You must ensure this file is included globally.
### Recommended import (Astro)
Import the stylesheet once in your main layout or entry file:
```ts
import "../cookiebanner.css";
```
This guarantees the banner styles are available on every page.
- This file is never overwritten
- All colours, spacing, radius, and animations are controlled via CSS variables
- Full visual control with zero JavaScript theming
---
## 🔐 Privacy
- No cookies before consent
- No tracking without permission
- No external requests
- Stored locally with TTL
---
## 🙏 Acknowledgements
Thanks to [@magicspon](https://github.com/magicspon) for assisting with the PR and issue triage.
---
## 📄 License
MIT © Velohost
---
## 🏢 Velohost
https://velohost.co.uk/

345
node_modules/astro-consent/dist/cli.cjs generated vendored Executable file
View File

@@ -0,0 +1,345 @@
#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const process = __importStar(require("node:process"));
const CWD = process.cwd();
const args = process.argv.slice(2);
const command = args[0] ?? "install";
const ASTRO_CONFIG_FILES = [
"astro.config.mjs",
"astro.config.ts",
"astro.config.js"
];
function findAstroConfig() {
for (const file of ASTRO_CONFIG_FILES) {
const fullPath = path.join(CWD, file);
if (fs.existsSync(fullPath))
return fullPath;
}
return null;
}
function exitWith(message, code = 1) {
console.error(`\n${message}\n`);
process.exit(code);
}
/* ─────────────────────────────────────
Locate astro.config
───────────────────────────────────── */
const configPath = findAstroConfig();
if (!configPath) {
exitWith("No astro.config.(mjs|ts|js) found. Run this in the root of an Astro project.");
}
let source = fs.readFileSync(configPath, "utf8");
/* ─────────────────────────────────────
REMOVE MODE
───────────────────────────────────── */
if (command === "remove") {
source = source
.replace(/\s*astroConsent\s*\([\s\S]*?\),?/gm, "")
.replace(/import\s+astroConsent\s+from\s+["']astro-consent["'];?\n?/, "");
fs.writeFileSync(configPath, source.trim() + "\n", "utf8");
const cssFile = path.join(CWD, "src", "cookiebanner.css");
if (fs.existsSync(cssFile))
fs.unlinkSync(cssFile);
console.log("\n🧹 astro-consent fully removed\n");
process.exit(0);
}
/* ─────────────────────────────────────
INSTALL MODE
───────────────────────────────────── */
const cssDir = path.join(CWD, "src");
const cssFile = path.join(cssDir, "cookiebanner.css");
if (!fs.existsSync(cssDir)) {
fs.mkdirSync(cssDir, { recursive: true });
}
/* ─────────────────────────────────────
Create CSS file ONCE (theme + structure)
───────────────────────────────────── */
if (!fs.existsSync(cssFile)) {
fs.writeFileSync(cssFile, `/* =========================================================
astro-consent — FULL THEME + STRUCTURE
This file is NEVER overwritten.
========================================================= */
:root {
--cb-font: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--cb-bg: var(--color-surface, #0c1220);
--cb-border: var(--color-border, rgba(255,255,255,0.08));
--cb-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
--cb-text: var(--color-text, #ffffff);
--cb-muted: var(--color-muted, #9ca3af);
--cb-link: var(--color-primary, #60a5fa);
--cb-radius: 16px;
--cb-btn-radius: 999px;
--cb-btn-padding: 10px 18px;
--cb-accept-bg: var(--color-cta, #22c55e);
--cb-accept-text: #ffffff;
--cb-reject-bg: var(--cb-border);
--cb-reject-text: var(--cb-text);
--cb-manage-bg: transparent;
--cb-manage-text: var(--cb-text);
--cb-manage-border: var(--cb-border);
--cb-modal-bg: var(--cb-bg);
--cb-modal-backdrop: rgba(15, 23, 42, 0.45);
--cb-modal-radius: 18px;
--cb-modal-width: 480px;
--cb-toggle-off-bg: var(--cb-border);
--cb-toggle-on-bg: var(--color-accent, #22c55e);
--cb-toggle-knob: #ffffff;
}
/* ===============================
Banner
=============================== */
#astro-consent-banner {
position: fixed;
left: 16px;
right: 16px;
bottom: 16px;
z-index: 9999;
font-family: var(--cb-font);
}
.cb-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px 24px;
display: flex;
gap: 24px;
justify-content: space-between;
align-items: center;
background: var(--cb-bg);
border-radius: var(--cb-radius);
border: 1px solid var(--cb-border);
box-shadow: var(--cb-shadow);
color: var(--cb-text);
}
.cb-title {
font-size: 16px;
font-weight: 600;
}
.cb-desc {
margin-top: 4px;
font-size: 14px;
color: var(--cb-muted);
}
.cb-desc a {
color: var(--cb-link);
text-decoration: none;
}
.cb-desc a:hover {
text-decoration: underline;
}
/* ===============================
Buttons
=============================== */
.cb-actions {
display: flex;
gap: 10px;
}
.cb-actions button {
padding: var(--cb-btn-padding);
border-radius: var(--cb-btn-radius);
border: 1px solid transparent;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.cb-accept {
background: var(--cb-accept-bg);
color: var(--cb-accept-text);
}
.cb-reject {
background: var(--cb-reject-bg);
color: var(--cb-reject-text);
}
.cb-manage {
background: var(--cb-manage-bg);
color: var(--cb-manage-text);
border: 1px solid var(--cb-manage-border);
}
/* ===============================
Modal
=============================== */
#astro-consent-modal {
position: fixed;
inset: 0;
background: var(--cb-modal-backdrop);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.cb-modal {
width: 100%;
max-width: var(--cb-modal-width);
background: var(--cb-modal-bg);
border-radius: var(--cb-modal-radius);
padding: 28px;
color: var(--cb-text);
border: 1px solid var(--cb-border);
}
/* ===============================
Modal rows (SPACING FIXED)
=============================== */
.cb-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 18px 0;
border-bottom: 1px solid var(--cb-border);
}
.cb-row:last-of-type {
border-bottom: 0;
padding-bottom: 32px;
}
/* ===============================
Modal actions
=============================== */
.cb-modal .cb-actions {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--cb-border);
display: flex;
justify-content: flex-end;
}
/* ===============================
Toggles
=============================== */
.cb-toggle {
width: 44px;
height: 24px;
background: var(--cb-toggle-off-bg);
border-radius: 999px;
position: relative;
cursor: pointer;
flex-shrink: 0;
}
.cb-toggle span {
position: absolute;
width: 18px;
height: 18px;
background: var(--cb-toggle-knob);
border-radius: 50%;
top: 3px;
left: 3px;
transition: transform 0.2s ease;
}
.cb-toggle.active {
background: var(--cb-toggle-on-bg);
}
.cb-toggle.active span {
transform: translateX(20px);
}
/* ===============================
Mobile
=============================== */
@media (max-width: 640px) {
.cb-container {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
.cb-actions {
justify-content: flex-end;
flex-wrap: wrap;
}
}
`, "utf8");
console.log("🎨 Created src/cookiebanner.css");
}
/* ─────────────────────────────────────
Inject Astro integration
───────────────────────────────────── */
if (!source.includes(`from "astro-consent"`)) {
source = `import astroConsent from "astro-consent";\n${source}`;
}
if (!source.includes("astroConsent(")) {
source = source.replace(/integrations\s*:\s*\[/, match => `${match}
astroConsent({
siteName: "My Website",
policyUrl: "/privacy",
consent: {
days: 30,
storageKey: "astro-consent"
}
}),
`);
}
fs.writeFileSync(configPath, source, "utf8");
console.log("\n🎉 astro-consent installed successfully");
console.log("👉 Style everything via src/cookiebanner.css");
console.log("👉 Run `astro-consent remove` to uninstall\n");

2
node_modules/astro-consent/dist/cli.d.ts generated vendored Normal file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

13
node_modules/astro-consent/dist/config/defaults.d.ts generated vendored Normal file
View File

@@ -0,0 +1,13 @@
import type { CookieBannerConfig } from "../types/config.js";
/**
* DEFAULT_CONFIG
* ---------------------------------------------------------
* Fallback values ONLY.
*
* These are used:
* - when the user omits a value
* - during internal normalisation
*
* User-provided config ALWAYS takes priority.
*/
export declare const DEFAULT_CONFIG: CookieBannerConfig;

40
node_modules/astro-consent/dist/config/defaults.js generated vendored Normal file
View File

@@ -0,0 +1,40 @@
/**
* DEFAULT_CONFIG
* ---------------------------------------------------------
* Fallback values ONLY.
*
* These are used:
* - when the user omits a value
* - during internal normalisation
*
* User-provided config ALWAYS takes priority.
*/
export const DEFAULT_CONFIG = {
siteName: "This website",
policyUrl: "/privacy",
consent: {
enabled: true,
// Number of days consent remains valid
days: 30,
// Must match runtime + frontend API
storageKey: "astro-consent"
},
categories: {
essential: {
label: "Essential",
description: "Required for the website to function correctly",
enabled: true,
readonly: true
},
analytics: {
label: "Analytics",
description: "Helps us understand how visitors use the site",
enabled: false
},
marketing: {
label: "Marketing",
description: "Used to deliver personalised ads",
enabled: false
}
}
};

View File

@@ -0,0 +1,7 @@
import type { CookieBannerConfig } from "../types/config.js";
/**
* Safely loads the user config file and merges it with defaults.
* User values always override defaults.
* Cache-busted to ensure updates are picked up during dev.
*/
export declare function loadUserConfig(projectRoot: string): Promise<CookieBannerConfig>;

65
node_modules/astro-consent/dist/config/loadConfig.js generated vendored Normal file
View File

@@ -0,0 +1,65 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { pathToFileURL } from "node:url";
import { DEFAULT_CONFIG } from "./defaults.js";
/**
* Safely loads the user config file and merges it with defaults.
* User values always override defaults.
* Cache-busted to ensure updates are picked up during dev.
*/
export async function loadUserConfig(projectRoot) {
const configPath = path.join(projectRoot, "src", "astro-consent", "config.ts");
let userConfig = {};
try {
const stat = fs.statSync(configPath);
const cacheBuster = `?v=${stat.mtimeMs}`;
const imported = await import(
/* @vite-ignore */
pathToFileURL(configPath).href + cacheBuster);
userConfig = imported?.default ?? {};
}
catch (err) {
console.warn("[astro-consent] Failed to load user config, falling back to defaults");
}
return {
siteName: userConfig.siteName ?? DEFAULT_CONFIG.siteName,
policyUrl: userConfig.policyUrl ?? DEFAULT_CONFIG.policyUrl,
consent: {
enabled: userConfig.consent?.enabled ??
DEFAULT_CONFIG.consent.enabled,
days: userConfig.consent?.days ??
DEFAULT_CONFIG.consent.days,
storageKey: userConfig.consent?.storageKey ??
DEFAULT_CONFIG.consent.storageKey
},
categories: mergeCategories(userConfig.categories, DEFAULT_CONFIG.categories)
};
}
/**
* Merge category config safely.
* - Defaults are preserved
* - User overrides win
* - Custom categories are supported
*/
function mergeCategories(userCategories, defaultCategories) {
const merged = {};
// Start with defaults
for (const key of Object.keys(defaultCategories)) {
merged[key] = {
...defaultCategories[key],
...(userCategories?.[key] ?? {})
};
}
// Add user-defined custom categories safely
if (userCategories) {
for (const key of Object.keys(userCategories)) {
if (!merged[key]) {
merged[key] = {
...userCategories[key],
enabled: userCategories[key].enabled ?? false
};
}
}
}
return merged;
}

15
node_modules/astro-consent/dist/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,15 @@
import type { AstroIntegration } from "astro";
export interface AstroConsentOptions {
siteName?: string;
policyUrl?: string;
consent?: {
days?: number;
storageKey?: string;
};
categories?: {
essential?: boolean;
analytics?: boolean;
marketing?: boolean;
};
}
export default function astroConsent(options?: AstroConsentOptions): AstroIntegration;

173
node_modules/astro-consent/dist/index.js generated vendored Normal file
View File

@@ -0,0 +1,173 @@
export default function astroConsent(options = {}) {
const siteName = options.siteName ?? "This website";
const policyUrl = options.policyUrl ?? "/privacy";
const consentDays = options.consent?.days ?? 30;
const storageKey = options.consent?.storageKey ?? "astro-consent";
const defaultCategories = {
essential: true,
analytics: false,
marketing: false,
...options.categories
};
const ttl = consentDays * 24 * 60 * 60 * 1000;
return {
name: "astro-consent",
hooks: {
"astro:config:setup": ({ injectScript }) => {
/* ─────────────────────────────────────
LOAD USER CSS (required)
───────────────────────────────────── */
injectScript("head-inline", `
(() => {
const id = "astro-consent-css";
if (document.getElementById(id)) return;
const link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
link.href = "/src/cookiebanner.css";
document.head.appendChild(link);
})();
`);
/* ─────────────────────────────────────
Consent runtime (NO CSS)
───────────────────────────────────── */
injectScript("page", `
(() => {
const KEY = "${storageKey}";
const TTL = ${ttl};
function now() { return Date.now(); }
function read() {
try {
const raw = localStorage.getItem(KEY);
if (!raw) return null;
const data = JSON.parse(raw);
if (data.expiresAt < now()) {
localStorage.removeItem(KEY);
return null;
}
return data;
} catch {
return null;
}
}
function write(categories) {
localStorage.setItem(KEY, JSON.stringify({
updatedAt: now(),
expiresAt: now() + TTL,
categories
}));
}
window.astroConsent = {
get: read,
set: write,
reset() {
localStorage.removeItem(KEY);
location.reload();
}
};
})();
`);
/* ─────────────────────────────────────
Banner + modal UI (NO CSS)
───────────────────────────────────── */
injectScript("page", `
(() => {
if (window.astroConsent.get()) return;
const state = { ...${JSON.stringify(defaultCategories)} };
const banner = document.createElement("div");
banner.id = "astro-consent-banner";
banner.innerHTML = \`
<div class="cb-container">
<div>
<div class="cb-title">${siteName} uses cookies</div>
<div class="cb-desc">
Choose how your data is used.
<a href="${policyUrl}">Learn more</a>
</div>
</div>
<div class="cb-actions">
<button class="cb-manage">Manage</button>
<button class="cb-reject">Reject</button>
<button class="cb-accept">Accept all</button>
</div>
</div>
\`;
document.body.appendChild(banner);
banner.querySelector(".cb-accept").onclick = () => {
window.astroConsent.set({ essential: true, analytics: true, marketing: true });
banner.remove();
};
banner.querySelector(".cb-reject").onclick = () => {
window.astroConsent.set({ essential: true });
banner.remove();
};
banner.querySelector(".cb-manage").onclick = openModal;
function openModal() {
const modal = document.createElement("div");
modal.id = "astro-consent-modal";
modal.innerHTML = \`
<div class="cb-modal">
<h3>Cookie preferences</h3>
<div class="cb-row">
<span>Essential</span>
<strong>Always on</strong>
</div>
<div class="cb-row">
<span>Analytics</span>
<div class="cb-toggle" data-key="analytics"><span></span></div>
</div>
<div class="cb-row">
<span>Marketing</span>
<div class="cb-toggle" data-key="marketing"><span></span></div>
</div>
<div class="cb-actions">
<button class="cb-accept">Save preferences</button>
</div>
</div>
\`;
document.body.appendChild(modal);
modal.querySelectorAll(".cb-toggle").forEach(toggle => {
const key = toggle.getAttribute("data-key");
if (state[key]) toggle.classList.add("active");
toggle.onclick = () => {
state[key] = !state[key];
toggle.classList.toggle("active");
};
});
modal.querySelector(".cb-accept").onclick = () => {
window.astroConsent.set({ essential: true, ...state });
modal.remove();
banner.remove();
};
modal.onclick = e => {
if (e.target === modal) modal.remove();
};
}
})();
`);
}
}
};
}

View File

@@ -0,0 +1 @@
export declare const DEFAULT_CSS = "\n:root {\n /* Layout */\n --cb-z-index: 9999;\n --cb-max-width: 960px;\n --cb-padding: 16px;\n --cb-gap: 12px;\n --cb-radius: 10px;\n\n /* Colours */\n --cb-bg: #111827;\n --cb-surface: #1f2933;\n --cb-text: #ffffff;\n --cb-muted: #9ca3af;\n --cb-border: #374151;\n --cb-accent: #6366f1;\n\n /* Buttons */\n --cb-btn-bg: var(--cb-accent);\n --cb-btn-text: #ffffff;\n --cb-btn-secondary-bg: transparent;\n --cb-btn-secondary-text: var(--cb-text);\n --cb-btn-border: var(--cb-border);\n\n /* Typography */\n --cb-font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\n --cb-title-size: 1rem;\n --cb-text-size: 0.875rem;\n}\n\n#astro-consent-banner {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: var(--cb-z-index);\n background: var(--cb-bg);\n color: var(--cb-text);\n font-family: var(--cb-font-family);\n border-top: 1px solid var(--cb-border);\n}\n\n#astro-consent-banner > * {\n max-width: var(--cb-max-width);\n margin: 0 auto;\n padding: var(--cb-padding);\n}\n\n#astro-consent-banner h2 {\n margin: 0 0 4px;\n font-size: var(--cb-title-size);\n}\n\n#astro-consent-banner p {\n margin: 0;\n font-size: var(--cb-text-size);\n color: var(--cb-muted);\n}\n\n#astro-consent-banner a {\n color: var(--cb-accent);\n text-decoration: underline;\n}\n\n.astro-consent-categories {\n display: grid;\n gap: var(--cb-gap);\n margin-top: var(--cb-gap);\n}\n\n.astro-consent-category {\n background: var(--cb-surface);\n padding: 12px;\n border-radius: var(--cb-radius);\n border: 1px solid var(--cb-border);\n}\n\n.astro-consent-label {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n}\n\n.astro-consent-description {\n margin: 4px 0 0 26px;\n font-size: 0.8rem;\n color: var(--cb-muted);\n}\n\n.astro-consent-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n margin-top: var(--cb-gap);\n flex-wrap: wrap;\n}\n\n.astro-consent-actions button {\n padding: 8px 14px;\n border-radius: var(--cb-radius);\n font-size: 0.875rem;\n cursor: pointer;\n border: 1px solid var(--cb-btn-border);\n}\n\n.astro-consent-actions button:first-child {\n background: var(--cb-btn-secondary-bg);\n color: var(--cb-btn-secondary-text);\n}\n\n.astro-consent-actions button:last-child {\n background: var(--cb-btn-bg);\n color: var(--cb-btn-text);\n border-color: var(--cb-btn-bg);\n}\n";

View File

@@ -0,0 +1,117 @@
export const DEFAULT_CSS = `
:root {
/* Layout */
--cb-z-index: 9999;
--cb-max-width: 960px;
--cb-padding: 16px;
--cb-gap: 12px;
--cb-radius: 10px;
/* Colours */
--cb-bg: #111827;
--cb-surface: #1f2933;
--cb-text: #ffffff;
--cb-muted: #9ca3af;
--cb-border: #374151;
--cb-accent: #6366f1;
/* Buttons */
--cb-btn-bg: var(--cb-accent);
--cb-btn-text: #ffffff;
--cb-btn-secondary-bg: transparent;
--cb-btn-secondary-text: var(--cb-text);
--cb-btn-border: var(--cb-border);
/* Typography */
--cb-font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--cb-title-size: 1rem;
--cb-text-size: 0.875rem;
}
#astro-consent-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--cb-z-index);
background: var(--cb-bg);
color: var(--cb-text);
font-family: var(--cb-font-family);
border-top: 1px solid var(--cb-border);
}
#astro-consent-banner > * {
max-width: var(--cb-max-width);
margin: 0 auto;
padding: var(--cb-padding);
}
#astro-consent-banner h2 {
margin: 0 0 4px;
font-size: var(--cb-title-size);
}
#astro-consent-banner p {
margin: 0;
font-size: var(--cb-text-size);
color: var(--cb-muted);
}
#astro-consent-banner a {
color: var(--cb-accent);
text-decoration: underline;
}
.astro-consent-categories {
display: grid;
gap: var(--cb-gap);
margin-top: var(--cb-gap);
}
.astro-consent-category {
background: var(--cb-surface);
padding: 12px;
border-radius: var(--cb-radius);
border: 1px solid var(--cb-border);
}
.astro-consent-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.astro-consent-description {
margin: 4px 0 0 26px;
font-size: 0.8rem;
color: var(--cb-muted);
}
.astro-consent-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: var(--cb-gap);
flex-wrap: wrap;
}
.astro-consent-actions button {
padding: 8px 14px;
border-radius: var(--cb-radius);
font-size: 0.875rem;
cursor: pointer;
border: 1px solid var(--cb-btn-border);
}
.astro-consent-actions button:first-child {
background: var(--cb-btn-secondary-bg);
color: var(--cb-btn-secondary-text);
}
.astro-consent-actions button:last-child {
background: var(--cb-btn-bg);
color: var(--cb-btn-text);
border-color: var(--cb-btn-bg);
}
`;

56
node_modules/astro-consent/dist/types/config.d.ts generated vendored Normal file
View File

@@ -0,0 +1,56 @@
export interface CookieCategory {
/**
* Whether this category is enabled by default
* (may be overridden by stored consent)
*/
enabled: boolean;
/**
* If true, this category cannot be disabled by the user
* and should always be treated as enabled
*/
readonly?: boolean;
/**
* Human-readable category label
*/
label: string;
/**
* Optional explanation shown to the user
*/
description?: string;
/**
* Optional UI hint:
* hide this category from the preferences UI
*/
hidden?: boolean;
}
export interface CookieBannerConfig {
/**
* Human-readable site name used in banner copy
*/
siteName: string;
/**
* URL to privacy / cookie policy
*/
policyUrl: string;
/**
* Consent storage configuration
*/
consent: {
/**
* Globally enable or disable the consent system
*/
enabled: boolean;
/**
* Number of days consent remains valid
*/
days: number;
/**
* LocalStorage key used to store consent
*/
storageKey: string;
};
/**
* Cookie categories (built-in or user-defined)
*/
categories: Record<string, CookieCategory>;
}

1
node_modules/astro-consent/dist/types/config.js generated vendored Normal file
View File

@@ -0,0 +1 @@
export {};

60
node_modules/astro-consent/package.json generated vendored Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "astro-consent",
"version": "1.0.17",
"description": "A privacy-first, GDPR-compliant cookie consent banner for Astro with a built-in preferences modal, zero dependencies, and full theme control.",
"type": "module",
"author": {
"name": "Velohost",
"url": "https://velohost.co.uk"
},
"license": "MIT",
"homepage": "https://velohost.co.uk/",
"repository": {
"type": "git",
"url": "git+https://github.com/velohost/astro-consent.git"
},
"bugs": {
"url": "https://github.com/velohost/astro-consent/issues"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"bin": {
"astro-consent": "dist/cli.cjs"
},
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"scripts": {
"build": "tsc -p tsconfig.json && tsc -p tsconfig.cli.json && mv dist/cli.js dist/cli.cjs"
},
"keywords": [
"astro",
"astro-integration",
"cookie-banner",
"cookie-consent",
"gdpr",
"privacy",
"analytics-consent",
"marketing-consent",
"withastro",
"velohost"
],
"peerDependencies": {
"astro": "^4.0.0 || ^5.0.0"
},
"devDependencies": {
"@types/node": "^25.2.1",
"astro": "^5.16.6"
},
"engines": {
"node": ">=18.0.0"
}
}