{"_id":"@alexpricedev/billet-cookie-consent","_rev":"5-a433256fa756b61fd61eb8b7bd3ffd87","name":"@alexpricedev/billet-cookie-consent","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@alexpricedev/billet-cookie-consent","version":"0.1.0","keywords":["cookie","consent","gdpr","banner","privacy","billet","bun"],"author":{"name":"Alex Price"},"license":"MIT","_id":"@alexpricedev/billet-cookie-consent@0.1.0","maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"homepage":"https://github.com/alexpricedev/billet-cookie-consent#readme","bugs":{"url":"https://github.com/alexpricedev/billet-cookie-consent/issues"},"dist":{"shasum":"bf0793affe9d33e087ca79b8747135bc31ab313a","tarball":"https://registry.npmjs.org/@alexpricedev/billet-cookie-consent/-/billet-cookie-consent-0.1.0.tgz","fileCount":10,"integrity":"sha512-Tm/VwmvfgCy44AVTvaI2oXwlVuPGVXBHLAXvJRM1mn3seZap2EHOe7Nf+qG0lEay0oKHIX0V3r4fEMmZWmW85Q==","signatures":[{"sig":"MEYCIQCDS7yXpdbbkH+Nyz4xb+nIbtty61gNihpk8CV+HMhp9gIhAN5GSOM8vn/3WTzyaH6IDOOnAq97p3wTeoP0dcGzRATC","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37547},"type":"module","types":"src/index.ts","module":"src/index.ts","engines":{"bun":">=1.0.0"},"exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./server":{"types":"./src/server.ts","import":"./src/server.ts","default":"./src/server.ts"},"./styles.css":"./src/styles.css"},"gitHead":"b3cb7d83d4ed69ceb950fc92182ae01be6138031","scripts":{"test":"bun test","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"alexpricedev","email":"npm@alexprice.dev"},"repository":{"url":"git+https://github.com/alexpricedev/billet-cookie-consent.git","type":"git"},"_npmVersion":"11.12.1","description":"Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"happy-dom":"^15.0.0","@types/bun":"latest","typescript":"^5.4.0","@happy-dom/global-registrator":"^20.9.0"},"_npmOperationalInternal":{"tmp":"tmp/billet-cookie-consent_0.1.0_1778862797121_0.4794320842290569","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@alexpricedev/billet-cookie-consent","version":"0.1.1","keywords":["cookie","consent","gdpr","banner","privacy","billet","bun"],"author":{"name":"Alex Price"},"license":"MIT","_id":"@alexpricedev/billet-cookie-consent@0.1.1","maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"homepage":"https://github.com/alexpricedev/billet-cookie-consent#readme","bugs":{"url":"https://github.com/alexpricedev/billet-cookie-consent/issues"},"dist":{"shasum":"1db52ea40c1f8016299465a94f39fb3f86b83a76","tarball":"https://registry.npmjs.org/@alexpricedev/billet-cookie-consent/-/billet-cookie-consent-0.1.1.tgz","fileCount":10,"integrity":"sha512-wjKtytMhXwe1wB9SkPikZ7O7VUE0qwK/SLctgP+tAWvTvtDbDInJYQWBzCd+Ty4V8P17pE2Bwx5sNmnlqMcESg==","signatures":[{"sig":"MEYCIQDxMTQqPjKe2DdyFpF2ua5zmJ/VLXKHx1jvAQRz+bP1BAIhANWA7z7yycPfXt/9G9ZZeN7LsfzD044TM22IgRuCVDqc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":39994},"type":"module","types":"src/index.ts","module":"src/index.ts","engines":{"bun":">=1.0.0"},"exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./server":{"types":"./src/server.ts","import":"./src/server.ts","default":"./src/server.ts"},"./styles.css":"./src/styles.css"},"gitHead":"1813e1e962a5cd993a7f77e3e8cc61906099754a","scripts":{"test":"bun test","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"alexpricedev","email":"npm@alexprice.dev"},"repository":{"url":"git+https://github.com/alexpricedev/billet-cookie-consent.git","type":"git"},"_npmVersion":"11.12.1","description":"Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"happy-dom":"^15.0.0","@types/bun":"latest","typescript":"^5.4.0","@happy-dom/global-registrator":"^20.9.0"},"_npmOperationalInternal":{"tmp":"tmp/billet-cookie-consent_0.1.1_1778869571074_0.33877052359554227","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@alexpricedev/billet-cookie-consent","version":"0.1.2","keywords":["cookie","consent","gdpr","banner","privacy","billet","bun"],"author":{"name":"Alex Price"},"license":"MIT","_id":"@alexpricedev/billet-cookie-consent@0.1.2","maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"homepage":"https://github.com/alexpricedev/billet-cookie-consent#readme","bugs":{"url":"https://github.com/alexpricedev/billet-cookie-consent/issues"},"dist":{"shasum":"b043c9dfe9cce1f731daf260a00d00773b9851ff","tarball":"https://registry.npmjs.org/@alexpricedev/billet-cookie-consent/-/billet-cookie-consent-0.1.2.tgz","fileCount":10,"integrity":"sha512-RwcseLBui/z45ltslSWFedstBPvnzV6LHp6CTLZJPm38eShcGnAj+xak7gdJWdxX4rVe0TzMAXy2hvZmSQTimA==","signatures":[{"sig":"MEYCIQDkdqPexbwDSGWewG2sAGWg4N3F6kxaf8ZpKv2fFMNV/QIhAMdVdFG3MsPNRW/C2/n8tBWGfQJrhvcVMt2R+zmbkYH9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40532},"type":"module","types":"src/index.ts","module":"src/index.ts","engines":{"bun":">=1.0.0"},"exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./server":{"types":"./src/server.ts","import":"./src/server.ts","default":"./src/server.ts"},"./styles.css":"./src/styles.css"},"gitHead":"850b53d43ed6a963f5cd1a24ef254f5f74cbeb97","scripts":{"test":"bun test","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"alexpricedev","email":"npm@alexprice.dev"},"repository":{"url":"git+https://github.com/alexpricedev/billet-cookie-consent.git","type":"git"},"_npmVersion":"11.12.1","description":"Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"happy-dom":"^15.0.0","@types/bun":"latest","typescript":"^5.4.0","@happy-dom/global-registrator":"^20.9.0"},"_npmOperationalInternal":{"tmp":"tmp/billet-cookie-consent_0.1.2_1778869669047_0.3845249612477937","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@alexpricedev/billet-cookie-consent","version":"0.1.3","keywords":["cookie","consent","gdpr","banner","privacy","billet","bun"],"author":{"name":"Alex Price"},"license":"MIT","_id":"@alexpricedev/billet-cookie-consent@0.1.3","maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"homepage":"https://github.com/alexpricedev/billet-cookie-consent#readme","bugs":{"url":"https://github.com/alexpricedev/billet-cookie-consent/issues"},"dist":{"shasum":"eaecf12b568894c029a596dd588e4439eb8ebe7a","tarball":"https://registry.npmjs.org/@alexpricedev/billet-cookie-consent/-/billet-cookie-consent-0.1.3.tgz","fileCount":10,"integrity":"sha512-tJXrhv2m+VL7vlQpvmgtRao9MzzzM9rudHRhYe99lPRCEULLRj3BYas79Yg2L9DzPK218HnmMMgmSVBFhing1Q==","signatures":[{"sig":"MEYCIQCdG1zan/qTJ2TpMEkOl/LrqbAlXM8Grv6CwKOVt2HqyAIhAPi/7bwbQXba4dPfInk3M1Ep0uIhcHJrKPPe3O8Vro2Q","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40999},"type":"module","types":"src/index.ts","module":"src/index.ts","engines":{"bun":">=1.0.0"},"exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./server":{"types":"./src/server.ts","import":"./src/server.ts","default":"./src/server.ts"},"./styles.css":"./src/styles.css"},"gitHead":"9071238b49502690446e15a1ef36ff57b2f73043","scripts":{"test":"bun test","check":"tsc --noEmit && bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"alexpricedev","email":"npm@alexprice.dev"},"repository":{"url":"git+https://github.com/alexpricedev/billet-cookie-consent.git","type":"git"},"_npmVersion":"11.12.1","description":"Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"happy-dom":"^15.0.0","@types/bun":"latest","typescript":"^5.4.0","@happy-dom/global-registrator":"^20.9.0"},"_npmOperationalInternal":{"tmp":"tmp/billet-cookie-consent_0.1.3_1778870807166_0.16652872089838744","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@alexpricedev/billet-cookie-consent","version":"0.2.0","description":"Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.","type":"module","module":"src/index.ts","types":"src/index.ts","exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./server":{"types":"./src/server.ts","import":"./src/server.ts","default":"./src/server.ts"},"./styles.css":"./src/styles.css"},"scripts":{"test":"bun test","typecheck":"tsc --noEmit","check":"tsc --noEmit && bun test"},"publishConfig":{"access":"public"},"engines":{"bun":">=1.0.0"},"license":"MIT","author":{"name":"Alex Price"},"repository":{"type":"git","url":"git+https://github.com/alexpricedev/billet-cookie-consent.git"},"bugs":{"url":"https://github.com/alexpricedev/billet-cookie-consent/issues"},"homepage":"https://github.com/alexpricedev/billet-cookie-consent#readme","keywords":["cookie","consent","gdpr","banner","privacy","billet","bun"],"devDependencies":{"@happy-dom/global-registrator":"^20.9.0","@types/bun":"latest","happy-dom":"^15.0.0","typescript":"^5.4.0"},"gitHead":"d181fe13edaab2f9ee4ed4e37e744b589fadf06e","_id":"@alexpricedev/billet-cookie-consent@0.2.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-1xPGgbVGRukcKrZCyk41BAYqxB82xySMnqBrSBAf/YNYNG2ILhi5upcA+8KCKkRWC+Qb+lhzGWI8Wv5p/ErclA==","shasum":"6b947b426b1ee6dbdbce1a576617637b039292c4","tarball":"https://registry.npmjs.org/@alexpricedev/billet-cookie-consent/-/billet-cookie-consent-0.2.0.tgz","fileCount":11,"unpackedSize":43910,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDGEV6qxgTKFKJMU5VOrOOmNNZ2cTqGywVw1icVNz++LwIhAPt/1Fw7+0VjC7JIk3cWqbffL40v4S9SH7VNlM8OxENZ"}]},"_npmUser":{"name":"alexpricedev","email":"npm@alexprice.dev"},"directories":{},"maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/billet-cookie-consent_0.2.0_1780677083338_0.21138015603026972"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-15T16:33:17.027Z","modified":"2026-06-05T16:31:23.590Z","0.1.0":"2026-05-15T16:33:17.261Z","0.1.1":"2026-05-15T18:26:11.213Z","0.1.2":"2026-05-15T18:27:49.185Z","0.1.3":"2026-05-15T18:46:47.347Z","0.2.0":"2026-06-05T16:31:23.480Z"},"bugs":{"url":"https://github.com/alexpricedev/billet-cookie-consent/issues"},"author":{"name":"Alex Price"},"license":"MIT","homepage":"https://github.com/alexpricedev/billet-cookie-consent#readme","keywords":["cookie","consent","gdpr","banner","privacy","billet","bun"],"repository":{"type":"git","url":"git+https://github.com/alexpricedev/billet-cookie-consent.git"},"description":"Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.","maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"readme":"# @alexpricedev/billet-cookie-consent\n\nGDPR-friendly cookie consent banner. Vanilla DOM, zero dependencies, scoped CSS tokens. Framework-agnostic — designed to drop into Billet but works anywhere.\n\n## Copy this to your coding agent\n\nPaste this into your agent (Claude Code, Cursor, etc.):\n\n```text\nInstall and wire up @alexpricedev/billet-cookie-consent in this repo.\n\n1. Read the package README first:\n   node_modules/@alexpricedev/billet-cookie-consent/README.md\n   (or https://github.com/alexpricedev/billet-cookie-consent#readme).\n   Follow its \"Wire up in 3 steps\" section. Adapt snippets to this\n   repo's controller / template signatures where they differ — don't\n   paste blindly.\n\n2. If this repo has a CLAUDE.md or AGENTS.md, read it and respect its\n   conventions (filenames, lint rules, where tests go). Those override\n   anything generic in the package README.\n\n3. Use three categories: necessary (required), analytics, marketing.\n   Set policyUrl to \"/privacy\" — and add a /privacy stub page if one\n   doesn't exist, with a co-located test.\n\n4. Add a \"Manage cookies\" footer button as shown in the README's\n   step 2. GDPR requires users be able to withdraw consent.\n\n5. Apply the README's Theming block: map this repo's existing\n   --color-* / --font-* (or equivalent) tokens to --cc-* on\n   [data-cc-root]. Otherwise the banner will ship in the package's\n   default look instead of matching the site.\n\n6. Verify with the repo's check/test commands, plus the /browse skill\n   (or equivalent headless browser tool): banner renders → reject →\n   reload-no-banner → manage-cookies-reopens-modal → /privacy returns\n   200. Do not scaffold a new browser-test harness — use whatever's\n   already wired in.\n\n7. Open a PR.\n   - Title: \"Add GDPR cookie consent\" — but check `git log` first\n     and adapt to the repo's commit convention if it uses one\n     (e.g. conventional commits → `feat(consent): add GDPR cookie\n     consent`). Commit conventions usually live in history, not in\n     CLAUDE.md.\n   - Body: short summary of what was wired + the verification\n     steps you performed.\n   - Screenshot: `gh pr create --body` can't embed local images.\n     Capture the banner with the browser tool and attach it as a\n     follow-up PR comment (`gh pr comment <n> --body-file …`) or\n     via the GitHub web UI.\n\nOut of scope: real analytics scripts, real privacy copy, translations,\nand server-side parseConsent wiring (defer until a real analytics\nscript is added).\n```\n\n## For AI agents\n\n- **Package**: `@alexpricedev/billet-cookie-consent`\n- **Install**: `bun add @alexpricedev/billet-cookie-consent`\n- **Peer deps**: none\n- **Bundle side effects**: appends one `<div data-cc-root>` to `<body>` on `init()`; writes one cookie (default name `cc_consent`)\n- **Server runtime**: any (`./server` export is a pure function over a Cookie header string)\n- **Wiring**: 3 client steps + 1 optional server step — see [Wire up in 3 steps](#wire-up-in-3-steps)\n- **Verify install**: `bun test` in the consumer should still pass; `document.querySelector('[data-cc-root]')` returns a node after init\n\n## Install\n\n```bash\nbun add @alexpricedev/billet-cookie-consent\n# or: npm install / pnpm add / yarn add\n```\n\n## Wire up in 3 steps\n\n### 1. Import the CSS once\n\nIn your global stylesheet (e.g. `src/client/style.css`):\n\n```css\n@import \"@alexpricedev/billet-cookie-consent/styles.css\";\n```\n\n### 2. Initialize the banner on the client\n\nCreate a file that runs on every page (e.g. `src/client/pages/consent.ts` for Billet, or your existing client entry):\n\n```ts\nimport { CookieConsent } from \"@alexpricedev/billet-cookie-consent\";\n\nconst consent = CookieConsent.init({\n  categories: [\n    { id: \"necessary\", label: \"Strictly necessary\", required: true,\n      description: \"Required for the site to function. Always on.\" },\n    { id: \"analytics\", label: \"Analytics\",\n      description: \"Helps us understand how the site is used.\" },\n    { id: \"marketing\", label: \"Marketing\",\n      description: \"Used to personalize ads and content.\" },\n  ],\n  policyUrl: \"/privacy\",\n});\n\nif (consent.has(\"analytics\")) loadAnalytics();\ndocument.addEventListener(\"cc:consent-granted\", (e) => {\n  if (e.detail.category === \"analytics\") loadAnalytics();\n});\n\n// \"Manage cookies\" footer trigger. Use a data attribute on the server-\n// rendered button — never inline onclick — so this fits a server-JSX +\n// islands pattern (e.g. Billet).\nfor (const el of document.querySelectorAll<HTMLElement>(\"[data-cc-open-prefs]\")) {\n  el.addEventListener(\"click\", () => CookieConsent.current()?.show());\n}\n\nfunction loadAnalytics(): void {\n  // Replace with your analytics loader. Idempotent: only run once.\n}\n```\n\nIn your footer template, render the trigger button:\n\n```tsx\n<button type=\"button\" data-cc-open-prefs>Manage cookies</button>\n```\n\nWire the init file into your client entry. In Billet, register it in `src/client/main.ts`:\n\n```ts\nimport \"./pages/consent\";\n```\n\n### 3. (Optional) Server-side gating\n\nTo avoid sending analytics `<script>` tags to users who haven't opted in, read the cookie in your request handler and pass the consent state to your template.\n\n```ts\nimport { parseConsent } from \"@alexpricedev/billet-cookie-consent/server\";\n\nconst consent = parseConsent(req.headers.get(\"cookie\"));\n// consent?.state.analytics === true | false\n```\n\nIn a Billet view controller, thread it into the template:\n\n```tsx\nimport { parseConsent } from \"@alexpricedev/billet-cookie-consent/server\";\n\nexport const home = {\n  index(req: Request): Response {\n    const consent = parseConsent(req.headers.get(\"cookie\"));\n    return render(<Home analytics={consent?.state.analytics === true} />);\n  },\n};\n```\n\nThen in `Layout` (or the page template):\n\n```tsx\n{props.analytics && <script src=\"https://plausible.io/js/script.js\" defer />}\n```\n\n## Decision table\n\nWhen wiring this up, you'll need to pick values. Sensible defaults are listed; change only when the listed condition applies.\n\n| Option | Default | Change when |\n|---|---|---|\n| `cookieName` | `cc_consent` | Your app already uses this name for something else |\n| `cookieMaxAgeDays` | `365` | Your jurisdiction requires a shorter re-prompt window |\n| `cookieSameSite` | `Lax` | You serve this widget on a third-party origin (use `None` + `secure: true`) |\n| `cookieSecure` | `true` on HTTPS, else `false` | You need to test on plain HTTP and want to force one or the other |\n| `position` | `bottom` | Host UI has a fixed bottom bar — use `top`, `bottom-left`, or `bottom-right` |\n| `policyUrl` | _none_ | You have a privacy policy page (recommended for GDPR compliance) |\n| `categories` | _required, no default_ | Always supply. Minimum: `[{ id: \"necessary\", label: \"Necessary\", required: true }]` |\n| `texts` | English defaults | You need a different language or wording |\n| `autoShow` | `true` | You want to gate rendering on additional logic before showing the banner |\n\n### Category fields\n\nEach entry in `categories` accepts:\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` _(required)_ | Stable identifier used in the cookie and in `has(id)`. Must be unique. |\n| `label` | `string` _(required)_ | Shown in the preferences modal. |\n| `description` | `string` | Optional helper text under the label. |\n| `required` | `boolean` | Forces the category **on** and **disables** its toggle. Use for strictly-necessary cookies. |\n| `default` | `boolean` | Pre-ticks the category on first visit while leaving the toggle **editable**. Counts as granted if the user saves without touching it. |\n\n**`default` vs `required`.** `required` is non-negotiable — always on, toggle disabled. `default` is a starting position the user can change — pre-ticked but editable, and \"Reject all\" turns it off. `required` wins: a category with both is always on. `default` only affects the first visit (and `reset()`); a returning user's saved choice is never overridden.\n\n> **Compliance caveat.** Under the GDPR/ePrivacy Directive, consent for non-essential cookies must be a deliberate opt-in. Pre-ticked boxes do **not** constitute valid consent (CJEU, *Planet49*, C-673/17). Only use `default: true` where you have a lawful basis and have considered the regulatory risk; in most cases consumers should opt in deliberately.\n\n## Theming\n\nAll design tokens are CSS custom properties scoped to `[data-cc-root]`. To theme, override them on the same selector (or any ancestor):\n\n```css\n[data-cc-root] {\n  --cc-bg:        var(--color-surface);\n  --cc-fg:        var(--color-text);\n  --cc-primary:   var(--color-primary);\n  --cc-primary-fg:#ffffff;\n  --cc-font:      var(--font-main);\n  --cc-radius:    8px;\n}\n```\n\nThe package never reads from host token names (`--color-*` etc) — you opt in by mapping your tokens to ours. Nothing leaks out of `[data-cc-root]`.\n\nAvailable tokens: `--cc-bg`, `--cc-fg`, `--cc-muted`, `--cc-primary`, `--cc-primary-fg`, `--cc-surface`, `--cc-border`, `--cc-radius`, `--cc-radius-sm`, `--cc-font`, `--cc-shadow`, `--cc-z`, `--cc-gap`, `--cc-pad`, `--cc-max-w`.\n\n## API reference\n\n### `CookieConsent.init(config): ConsentController`\n\nRenders the banner if no valid cookie is present, otherwise stays silent. Throws if called twice — call `controller.destroy()` first if you need to re-init.\n\n**Side effects**: appends `<div data-cc-root>` to `<body>`; reads `document.cookie`.\n\n### `CookieConsent.current(): ConsentController | null`\n\nReturns the active controller (or `null` if `init` hasn't run yet). Use this from code that doesn't have a direct reference to the controller — e.g. a global \"Manage cookies\" click handler.\n\n### `controller.has(category): boolean`\n\nTrue iff the category is currently granted. Required categories are always `true` once a record exists.\n\n### `controller.get(): ConsentMap`\n\nReturns a copy of the current consent map: `{ [categoryId]: boolean }`.\n\n### `controller.record(): ConsentRecord | null`\n\nReturns the full stored record (`{ v, ts, cats }`) or `null` if the user hasn't decided yet.\n\n### `controller.acceptAll()` / `controller.rejectAll()`\n\nPersist all-on / all-off (required categories stay on). Dismisses banner and modal. Fires events.\n\n### `controller.save(state: ConsentMap)`\n\nPersist a specific consent map. Required categories are forced on. Fires events.\n\n### `controller.show()`\n\nOpen the preferences modal. Use this for \"Manage cookies\" links in your footer (see step 2 above for the data-attribute pattern).\n\n### `controller.reset()`\n\nClear the cookie and re-show the banner. For testing or \"withdraw consent\" flows.\n\n### `controller.destroy()`\n\nRemove DOM, allow `init()` to be called again. Useful for HMR.\n\n### `parseConsent(cookieHeaderOrJar, opts?)` (server)\n\n```ts\nimport { parseConsent } from \"@alexpricedev/billet-cookie-consent/server\";\n\nparseConsent(req.headers.get(\"cookie\"));         // → { state, record } | null\nparseConsent({ cc_consent: \"...\" });             // accepts pre-parsed jar\nparseConsent(header, { cookieName: \"custom\" }); // override cookie name\n```\n\nPure function. Returns `null` on missing/malformed/wrong-version cookie.\n\n### `hasConsent(cookieHeaderOrJar, category, opts?)` (server)\n\nConvenience boolean check: `hasConsent(req.headers.get(\"cookie\"), \"analytics\")`.\n\n## Events\n\nDispatched on `document` as `CustomEvent`:\n\n| Event | Detail |\n|---|---|\n| `cc:consent-changed` | `{ state: ConsentMap, record: ConsentRecord }` — fires on every save |\n| `cc:consent-granted` | `{ category: string }` — fires once per newly-granted category |\n| `cc:consent-revoked` | `{ category: string }` — fires once per newly-revoked category |\n\n```ts\ndocument.addEventListener(\"cc:consent-granted\", (e) => {\n  if (e.detail.category === \"analytics\") loadAnalytics();\n});\n```\n\n## Cookie format\n\nThe cookie value is URL-encoded JSON:\n\n```json\n{ \"v\": 1, \"ts\": 1736899200000, \"cats\": { \"necessary\": true, \"analytics\": false } }\n```\n\n- `v` is the schema version. Future versions invalidate older cookies (user is re-prompted).\n- `ts` is the decision timestamp in unix ms.\n- `cats` maps every configured category id to a boolean.\n\nIf the configured category set ever drifts from the stored set (you add a new category), the cookie is treated as stale and the banner re-prompts.\n\n## Common pitfalls\n\n- **Do not** set `HttpOnly` on the consent cookie. The client must read it to know whether to load gated scripts.\n- **Do not** call `CookieConsent.init()` more than once per page — it throws. Use `controller.destroy()` first if you must.\n- **Do not** use inline `onclick=` for the \"Manage cookies\" button in a server-JSX codebase. Use the data-attribute + client-side listener pattern shown in step 2.\n- **Do not** import `parseConsent` \"for discoverability\" without using it — strict TypeScript / lint setups (`noUnusedLocals`) will reject it. Add a code comment instead, and import only when you have a real script to gate.\n- **Do not** assume `parseConsent` returning `null` means \"no consent\". It means \"no decision yet\" — treat it the same as all-off but consider whether to re-prompt.\n- **The package never sets analytics cookies for you.** It only stores the user's choice. You are responsible for conditionally loading your own analytics/marketing scripts.\n- **Required categories are forced on** at write time. If a user toggles `necessary` off in dev tools, the next save re-enables it.\n\n## Verifying the install\n\nAfter wiring up, run in your host project:\n\n```bash\nbun test            # your existing tests still pass\nbun run check       # your existing lint/typecheck still passes\n```\n\nThen in the browser (or via a headless browser tool):\n\n1. Hard-reload — banner appears.\n2. Click \"Reject all\" — banner disappears; `document.cookie` contains `cc_consent=…`.\n3. Reload — no banner.\n4. Run `document.querySelector('[data-cc-root]')` — returns a node.\n5. Open preferences via your footer link — modal appears, ESC closes it.\n\n## Development\n\n```bash\nbun install\nbun test            # 60+ tests, ~99% line coverage\nbun run typecheck   # tsc --noEmit, strict mode\n```\n\n## License\n\nMIT © Alex Price\n","readmeFilename":"README.md"}