@bulut5/ck 0.1.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/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # ck
2
+
3
+ A lightweight, dependency-free cookie-consent banner for React that signals
4
+ [Google Consent Mode v2](https://developers.google.com/tag-platform/security/guides/consent)
5
+ and Microsoft UET. Default-deny, granular categories, SSR-safe, themeable with Tailwind.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i @bulut5/ck
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import { ConsentProvider, CookieConsent } from "@bulut5/ck";
17
+
18
+ const config = {
19
+ brand: "example",
20
+ locale: "en-GB",
21
+ privacyPolicyUrl: "/privacy",
22
+ copy: { /* … see BrandConfig */ },
23
+ };
24
+
25
+ export default function App() {
26
+ return (
27
+ <ConsentProvider config={config}>
28
+ {/* your app */}
29
+ <CookieConsent />
30
+ </ConsentProvider>
31
+ );
32
+ }
33
+ ```
34
+
35
+ The host page should set the Google Consent Mode default to `denied` inline in `<head>` before
36
+ the bundle loads; the banner flips signals to `granted` on consent. Tailwind consumers should add
37
+ `./node_modules/@bulut5/ck/dist/**/*.{js,mjs}` to their `content` so the banner classes compile.
38
+
39
+ MIT licensed.
@@ -0,0 +1,86 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type ConsentCategory = "marketing" | "analytics" | "personalization";
5
+ type ConsentDecision = Record<ConsentCategory, boolean>;
6
+ interface CategoryCopy {
7
+ label: string;
8
+ description: string;
9
+ }
10
+ interface ConsentCopy {
11
+ title: string;
12
+ body: string;
13
+ privacyLinkText: string;
14
+ acceptAll: string;
15
+ rejectAll: string;
16
+ select: string;
17
+ save: string;
18
+ back: string;
19
+ /** Accessible label for the floating re-open badge. */
20
+ badgeLabel: string;
21
+ categories: Record<ConsentCategory, CategoryCopy>;
22
+ }
23
+ interface BrandConfig {
24
+ brand: string;
25
+ locale: string;
26
+ privacyPolicyUrl: string;
27
+ copy: ConsentCopy;
28
+ /** Which ad-platform consent signals to emit. Defaults: google true, bing true. */
29
+ signals?: {
30
+ google?: boolean;
31
+ bing?: boolean;
32
+ };
33
+ /** Bump to force a re-prompt when the policy / categories change. Default 1. */
34
+ version?: number;
35
+ }
36
+ interface StoredConsent {
37
+ decision: ConsentDecision;
38
+ version: number;
39
+ ts: number;
40
+ }
41
+
42
+ interface ConsentContextValue {
43
+ /** Current decision, or null until the user has chosen. */
44
+ decision: ConsentDecision | null;
45
+ decided: boolean;
46
+ /** True only after the first client effect — gates SSR/first-render output. */
47
+ mounted: boolean;
48
+ /** Whether the card is open (undecided, or re-opened via the badge). */
49
+ open: boolean;
50
+ config: BrandConfig;
51
+ acceptAll: () => void;
52
+ rejectAll: () => void;
53
+ saveSelection: (decision: ConsentDecision) => void;
54
+ reopen: () => void;
55
+ close: () => void;
56
+ }
57
+ declare function ConsentProvider({ config, children, }: {
58
+ config: BrandConfig;
59
+ children?: ReactNode;
60
+ }): react.JSX.Element;
61
+
62
+ declare function useConsent(): ConsentContextValue;
63
+
64
+ /**
65
+ * Renders the consent UI: a sleek bottom-right popup card when undecided (or
66
+ * re-opened), otherwise a small floating badge in the same corner that re-opens
67
+ * it. SSR-safe — returns null until mounted on the client.
68
+ */
69
+ declare function CookieConsent(): react.JSX.Element | null;
70
+
71
+ declare const DENY_ALL: ConsentDecision;
72
+ declare const GRANT_ALL: ConsentDecision;
73
+ declare function loadConsent(version: number): StoredConsent | null;
74
+ declare function saveConsent(decision: ConsentDecision, version: number): StoredConsent;
75
+ /**
76
+ * Emit consent signals to the configured ad platforms. gtag / uetq are globals
77
+ * the host page sets up in <head>; we only update their consent state.
78
+ */
79
+ declare function applyConsent(decision: ConsentDecision, signals?: {
80
+ google?: boolean;
81
+ bing?: boolean;
82
+ }): void;
83
+ /** Load any stored decision and re-apply its signals. Returns the decision or null. */
84
+ declare function applyStoredConsent(config: BrandConfig): ConsentDecision | null;
85
+
86
+ export { type BrandConfig, type CategoryCopy, type ConsentCategory, type ConsentContextValue, type ConsentCopy, type ConsentDecision, ConsentProvider, CookieConsent, DENY_ALL, GRANT_ALL, type StoredConsent, applyConsent, applyStoredConsent, loadConsent, saveConsent, useConsent };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import {createContext,useState,useEffect,useCallback,useContext}from'react';import {jsx,jsxs}from'react/jsx-runtime';var B="ck.consent",u={marketing:false,analytics:false,personalization:false},S={marketing:true,analytics:true,personalization:true};function A(){return typeof window<"u"}function v(e){if(!A())return null;try{let o=window.localStorage.getItem(B);if(!o)return null;let t=JSON.parse(o);return !t||t.version!==e||!t.decision?null:t}catch{return null}}function D(e,o){let t={decision:e,version:o,ts:Date.now()};if(A())try{window.localStorage.setItem(B,JSON.stringify(t));}catch{}return t}function c(e){return e?"granted":"denied"}function g(e,o={}){if(!A())return;let t=window,{google:l=true,bing:d=true}=o;l&&typeof t.gtag=="function"&&t.gtag("consent","update",{ad_storage:c(e.marketing),ad_user_data:c(e.marketing),ad_personalization:c(e.marketing),analytics_storage:c(e.analytics),personalization_storage:c(e.personalization)}),d&&(t.uetq=t.uetq||[],t.uetq.push("consent","update",{ad_storage:c(e.marketing)}));}function T(e){let o=v(e.version??1);return o?(g(o.decision,e.signals),o.decision):null}var _=createContext(null);function O({config:e,children:o}){let t=e.version??1,[l,d]=useState(null),[k,N]=useState(false),[h,f]=useState(false);useEffect(()=>{let s=v(t);s&&(d(s.decision),g(s.decision,e.signals)),N(true);},[t]);let a=useCallback(s=>{D(s,t),g(s,e.signals),d(s),f(false);},[t,e.signals]),b=useCallback(()=>a({...S}),[a]),C=useCallback(()=>a({...u}),[a]),x=useCallback(s=>a(s),[a]),y=useCallback(()=>f(true),[]),r=useCallback(()=>f(false),[]),w={decision:l,decided:l!==null,mounted:k,open:h,config:e,acceptAll:b,rejectAll:C,saveSelection:x,reopen:y,close:r};return jsx(_.Provider,{value:w,children:o})}function E(){let e=useContext(_);if(!e)throw new Error("useConsent must be used within <ConsentProvider>");return e}var j=["marketing","analytics","personalization"];function I(){let{mounted:e,decided:o,open:t,decision:l,config:d,acceptAll:k,rejectAll:N,saveSelection:h,reopen:f,close:a}=E(),[b,C]=useState(false),[x,y]=useState({...u});if(!e)return null;let{copy:r}=d;if(o&&!t)return jsx("button",{type:"button",onClick:f,"aria-label":r.badgeLabel,className:"fixed bottom-4 right-4 z-[60] flex h-11 w-11 items-center justify-center rounded-full border border-border bg-background text-foreground shadow-lg transition hover:opacity-90",children:jsx("span",{"aria-hidden":true,className:"text-lg",children:"\u{1F36A}"})});let w=()=>{y(l?{...l}:{...u}),C(true);},s=i=>y(z=>({...z,[i]:!z[i]})),R=()=>{C(false),a();};return jsxs("div",{role:"dialog","aria-label":r.title,className:"fixed bottom-4 right-4 z-[60] w-[calc(100vw-2rem)] max-w-sm rounded-xl border border-border bg-background p-5 text-foreground shadow-2xl",children:[jsx("div",{className:"mb-2 text-base font-semibold",children:r.title}),jsx("p",{className:"mb-3 text-sm leading-relaxed text-muted-foreground",children:r.body}),jsx("a",{href:d.privacyPolicyUrl,target:"_blank",rel:"noopener noreferrer",className:"mb-4 inline-block text-sm font-medium underline underline-offset-2 hover:opacity-80",children:r.privacyLinkText}),b&&jsx("div",{className:"mb-4 space-y-3 border-t border-border pt-4",children:j.map(i=>jsxs("label",{className:"flex items-start gap-3 text-sm",children:[jsx("input",{type:"checkbox",checked:x[i],onChange:()=>s(i),className:"mt-0.5 h-4 w-4 shrink-0 accent-primary"}),jsxs("span",{children:[jsx("span",{className:"font-medium",children:r.categories[i].label}),jsx("span",{className:"block text-muted-foreground",children:r.categories[i].description})]})]},i))}),b?jsxs("div",{className:"flex gap-2",children:[jsx("button",{type:"button",onClick:()=>h(x),className:"flex-1 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition hover:opacity-90",children:r.save}),jsx("button",{type:"button",onClick:()=>C(false),className:"rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition hover:bg-muted",children:r.back})]}):jsxs("div",{className:"flex flex-col gap-2",children:[jsxs("div",{className:"flex gap-2",children:[jsx("button",{type:"button",onClick:k,className:"flex-1 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition hover:opacity-90",children:r.acceptAll}),jsx("button",{type:"button",onClick:N,className:"flex-1 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition hover:bg-muted",children:r.rejectAll})]}),jsx("button",{type:"button",onClick:w,className:"rounded-md px-3 py-2 text-sm font-medium text-muted-foreground underline underline-offset-2 transition hover:text-foreground",children:r.select}),o&&jsx("button",{type:"button",onClick:R,"aria-label":"Close",className:"absolute right-3 top-3 text-muted-foreground transition hover:text-foreground",children:"\u2715"})]})]})}export{O as ConsentProvider,I as CookieConsent,u as DENY_ALL,S as GRANT_ALL,g as applyConsent,T as applyStoredConsent,v as loadConsent,D as saveConsent,E as useConsent};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@bulut5/ck",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight cookie-consent banner with Google Consent Mode v2 + Microsoft UET signaling for React.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "tsup"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=18",
31
+ "react-dom": ">=18"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^18.3.3",
35
+ "react": "^18.3.1",
36
+ "react-dom": "^18.3.1",
37
+ "tsup": "^8.3.0",
38
+ "typescript": "^5.5.0"
39
+ }
40
+ }