@caprail-dev/consent 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 +156 -0
- package/dist/cn.d.ts +8 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/cn.js +11 -0
- package/dist/cn.js.map +1 -0
- package/dist/consent-banner.d.ts +3 -0
- package/dist/consent-banner.d.ts.map +1 -0
- package/dist/consent-banner.js +58 -0
- package/dist/consent-banner.js.map +1 -0
- package/dist/consent-provider.d.ts +14 -0
- package/dist/consent-provider.d.ts.map +1 -0
- package/dist/consent-provider.js +178 -0
- package/dist/consent-provider.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/regions.d.ts +18 -0
- package/dist/regions.d.ts.map +1 -0
- package/dist/regions.js +62 -0
- package/dist/regions.js.map +1 -0
- package/dist/service-registry.d.ts +14 -0
- package/dist/service-registry.d.ts.map +1 -0
- package/dist/service-registry.js +22 -0
- package/dist/service-registry.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/button.d.ts.map +1 -0
- package/dist/ui/button.js +19 -0
- package/dist/ui/button.js.map +1 -0
- package/dist/ui/dialog.d.ts +11 -0
- package/dist/ui/dialog.d.ts.map +1 -0
- package/dist/ui/dialog.js +30 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/switch.d.ts +5 -0
- package/dist/ui/switch.d.ts.map +1 -0
- package/dist/ui/switch.js +14 -0
- package/dist/ui/switch.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @caprail-dev/consent
|
|
2
|
+
|
|
3
|
+
A region-aware cookie-consent banner **and** consent-state layer for React, made
|
|
4
|
+
to pair with [`@caprail-dev/analytics`](https://www.npmjs.com/package/@caprail-dev/analytics)
|
|
5
|
+
— but analytics-agnostic, so it gates any tracker (GA, PostHog, your own).
|
|
6
|
+
|
|
7
|
+
- **Consent matrix as state** — `useConsent()` gives any component the live
|
|
8
|
+
per-category grants and the actions to change them.
|
|
9
|
+
- **Shadcn + Tailwind** — styled with standard Shadcn tokens
|
|
10
|
+
(`bg-background`, `text-foreground`, `border`, `bg-primary`, …), so it inherits
|
|
11
|
+
_your_ theme. Override anything with `className`.
|
|
12
|
+
- **Region-aware** — pass the visitor's country and it switches between opt-in
|
|
13
|
+
(EEA/UK/CH/BR), opt-out (US-CA, with a "Do Not Sell/Share" link), and notice.
|
|
14
|
+
- **Mobile-first** — bottom-anchored, stacked buttons that go inline on `sm:`,
|
|
15
|
+
safe-area inset for notch devices, reduced-motion aware, focus-trapped dialog.
|
|
16
|
+
- **Extendable** — register third-party services with a `Loader` that mounts only
|
|
17
|
+
while its category is granted; nothing is bundled.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add @caprail-dev/consent
|
|
23
|
+
# peers (you already have these in a React app)
|
|
24
|
+
bun add react react-dom
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup (two required steps)
|
|
28
|
+
|
|
29
|
+
**1. Standard Shadcn tokens.** The components use `bg-background`,
|
|
30
|
+
`text-foreground`, `border`, `bg-primary`, `text-muted-foreground`, `bg-input`,
|
|
31
|
+
`ring-ring`, `bg-accent`. Any Shadcn project already defines these; if you don't
|
|
32
|
+
use Shadcn, define the same CSS variables.
|
|
33
|
+
|
|
34
|
+
**2. Let Tailwind scan the package.** Tailwind v4 does not scan `node_modules`
|
|
35
|
+
by default, so add this to the CSS file where you import Tailwind:
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
@import "tailwindcss";
|
|
39
|
+
@source "../node_modules/@caprail-dev/consent/dist";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
(Adjust the relative path to your CSS file's location.)
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { ConsentProvider } from "@caprail-dev/consent";
|
|
48
|
+
|
|
49
|
+
export default function RootLayout({ children }) {
|
|
50
|
+
return (
|
|
51
|
+
<ConsentProvider /* country="DE" region="BY" */>{children}</ConsentProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The provider renders the banner and the consent-gated services itself — one tag.
|
|
57
|
+
|
|
58
|
+
### Region detection
|
|
59
|
+
|
|
60
|
+
The package never fetches geo. Pass the visitor's country (and subdivision for
|
|
61
|
+
US-CA) from your own edge — e.g. in a Next.js layout from the request headers:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { headers } from "next/headers";
|
|
65
|
+
|
|
66
|
+
const h = await headers();
|
|
67
|
+
<ConsentProvider
|
|
68
|
+
country={h.get("x-vercel-ip-country")}
|
|
69
|
+
region={h.get("x-vercel-ip-country-region")}
|
|
70
|
+
>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
With no country the mode is `notice` and the banner is informational.
|
|
74
|
+
|
|
75
|
+
### Reading and reacting to consent
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
"use client";
|
|
79
|
+
import { useConsent } from "@caprail-dev/consent";
|
|
80
|
+
|
|
81
|
+
function Analytics() {
|
|
82
|
+
const { consent } = useConsent();
|
|
83
|
+
if (!consent.analytics) return null;
|
|
84
|
+
return /* … load analytics … */;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Non-React listeners can use the `caprail:consent` window event, dispatched on
|
|
89
|
+
every settled change:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
window.addEventListener("caprail:consent", (e) => {
|
|
93
|
+
const { analytics, marketing } = (e as CustomEvent).detail;
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Gating third-party services
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import Script from "next/script";
|
|
101
|
+
import { ConsentProvider, type ServiceDefinition } from "@caprail-dev/consent";
|
|
102
|
+
|
|
103
|
+
const services: ServiceDefinition[] = [
|
|
104
|
+
{
|
|
105
|
+
id: "ga",
|
|
106
|
+
name: "Google Analytics",
|
|
107
|
+
category: "analytics",
|
|
108
|
+
Loader: ({ cookieless }) => (
|
|
109
|
+
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" />
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
<ConsentProvider services={services}>{children}</ConsentProvider>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A service's `Loader` mounts only while its category is granted and unmounts on
|
|
118
|
+
revoke. `cookieless` (from the provider's `cookieless` prop) lets a loader run
|
|
119
|
+
its SDK in a memory-only mode.
|
|
120
|
+
|
|
121
|
+
### Custom categories
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<ConsentProvider
|
|
125
|
+
categories={[
|
|
126
|
+
{ id: "necessary", label: "Necessary", description: "Always on." },
|
|
127
|
+
{ id: "analytics", label: "Analytics", description: "Usage insights." },
|
|
128
|
+
{ id: "ads", label: "Advertising", description: "Personalized ads." },
|
|
129
|
+
]}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`necessary` is always present and locked. `ConsentState` is keyed by your ids.
|
|
134
|
+
|
|
135
|
+
## `ConsentProvider` props
|
|
136
|
+
|
|
137
|
+
| Prop | Type | Default | Notes |
|
|
138
|
+
| ------------- | --------------------- | ----------------------------- | ----------------------------------------------- |
|
|
139
|
+
| `country` | `string \| null` | `null` | ISO-3166-1 alpha-2, e.g. `x-vercel-ip-country`. |
|
|
140
|
+
| `region` | `string \| null` | `null` | Subdivision, used for US-CA. |
|
|
141
|
+
| `categories` | `CategoryConfig[]` | necessary/analytics/marketing | Toggles shown in "Manage". |
|
|
142
|
+
| `services` | `ServiceDefinition[]` | `[]` | Trackers to gate + list. |
|
|
143
|
+
| `forceBanner` | `boolean` | `false` | Show even with no non-essential category. |
|
|
144
|
+
| `cookieless` | `boolean` | `false` | Forwarded to each `Loader`. |
|
|
145
|
+
| `cookieName` | `string` | `"caprail_consent"` | First-party cookie name. |
|
|
146
|
+
| `onChange` | `(state) => void` | — | Called after every settled change. |
|
|
147
|
+
|
|
148
|
+
## `useConsent()`
|
|
149
|
+
|
|
150
|
+
Returns `{ consent, mode, region, categories, categoriesInUse, showBanner,
|
|
151
|
+
acceptAll, rejectAll, setCategory, save, openBanner }`. Call `openBanner()` from a
|
|
152
|
+
footer "Cookie settings" link to let visitors revise a decision.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
package/dist/cn.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ClassValue } from "clsx";
|
|
2
|
+
/**
|
|
3
|
+
* Local `cn` — the package ships its own copy since it can't import the host
|
|
4
|
+
* app's `@/lib/utils`. Same recipe: clsx + tailwind-merge so consumer
|
|
5
|
+
* `className` overrides win over the built-in utilities.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
|
8
|
+
//# sourceMappingURL=cn.d.ts.map
|
package/dist/cn.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cn.d.ts","sourceRoot":"","sources":["../src/cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAG7C;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
|
package/dist/cn.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
/**
|
|
4
|
+
* Local `cn` — the package ships its own copy since it can't import the host
|
|
5
|
+
* app's `@/lib/utils`. Same recipe: clsx + tailwind-merge so consumer
|
|
6
|
+
* `className` overrides win over the built-in utilities.
|
|
7
|
+
*/
|
|
8
|
+
export function cn(...inputs) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=cn.js.map
|
package/dist/cn.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cn.js","sourceRoot":"","sources":["../src/cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,MAAM,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,EAAE,CAAC,GAAG,MAAoB;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-banner.d.ts","sourceRoot":"","sources":["../src/consent-banner.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAwB/B,wBAAgB,aAAa,6BAsD5B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useConsentContext } from "./consent-provider";
|
|
5
|
+
import { Button } from "./ui/button";
|
|
6
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "./ui/dialog";
|
|
7
|
+
import { Switch } from "./ui/switch";
|
|
8
|
+
// Region-aware cookie banner: a token-themed, bottom-anchored card with
|
|
9
|
+
// accept-all / reject-all / manage, plus a CCPA "do not sell or share"
|
|
10
|
+
// affordance in opt-out regions. It only renders when the provider says a banner
|
|
11
|
+
// is needed and the visitor hasn't decided. Mobile-first: stacked buttons that
|
|
12
|
+
// go inline on `sm:`, a safe-area inset for notch devices, and reduced-motion
|
|
13
|
+
// support. The provider renders it; it can also be dropped in standalone.
|
|
14
|
+
/** Self-contained slide-in so consumers need no extra keyframes or plugins. */
|
|
15
|
+
const SLIDE_IN_KEYFRAMES = `@keyframes caprail-consent-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}`;
|
|
16
|
+
export function ConsentBanner() {
|
|
17
|
+
const { showBanner, mode, categories, categoriesInUse, acceptAll, rejectAll, } = useConsentContext();
|
|
18
|
+
const [manageOpen, setManageOpen] = React.useState(false);
|
|
19
|
+
if (!showBanner)
|
|
20
|
+
return null;
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { children: SLIDE_IN_KEYFRAMES }), _jsx("div", { role: "dialog", "aria-label": "Cookie consent", className: "fixed inset-x-0 bottom-0 z-50 flex justify-center p-3 motion-safe:animate-[caprail-consent-in_300ms_cubic-bezier(0.22,1,0.36,1)] sm:p-4", style: { paddingBottom: "calc(env(safe-area-inset-bottom) + 0.75rem)" }, children: _jsxs("div", { className: "border-border bg-background flex w-full max-w-3xl flex-col gap-3 rounded-2xl border p-4 shadow-xl sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:p-5", children: [_jsx("p", { className: "text-muted-foreground text-sm", children: "We use cookies and similar tools to measure and improve your experience. You can accept, reject, or choose what to allow." }), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setManageOpen(true), children: "Manage" }), _jsx(Button, { variant: "outline", size: "sm", onClick: rejectAll, children: mode === "opt-out" ? "Reject" : "Reject all" }), _jsx(Button, { size: "sm", onClick: acceptAll, children: "Accept all" })] })] }) }), manageOpen && (_jsx(ManageDialog, { onClose: () => setManageOpen(false), categories: categories, rowIds: ["necessary", ...categoriesInUse] }))] }));
|
|
22
|
+
}
|
|
23
|
+
function ManageDialog({ onClose, categories, rowIds, }) {
|
|
24
|
+
const { consent, mode, save, services } = useConsentContext();
|
|
25
|
+
// Staged draft so toggling is local until "Save" — avoids committing (and
|
|
26
|
+
// firing loaders) on every flip. The dialog mounts only while open, so the
|
|
27
|
+
// initializer seeds the draft from the live state on each open.
|
|
28
|
+
const [draft, setDraft] = React.useState(consent);
|
|
29
|
+
const byId = new Map(categories.map((c) => [c.id, c]));
|
|
30
|
+
return (_jsx(Dialog, { open: true, onOpenChange: (open) => {
|
|
31
|
+
if (!open)
|
|
32
|
+
onClose();
|
|
33
|
+
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Privacy preferences" }), _jsx(DialogDescription, { children: "Choose which categories to allow. Necessary items are always on." })] }), _jsx("div", { className: "flex flex-col gap-3", children: rowIds.map((id) => {
|
|
34
|
+
const config = byId.get(id);
|
|
35
|
+
if (!config)
|
|
36
|
+
return null;
|
|
37
|
+
const locked = !!config.locked;
|
|
38
|
+
return (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-foreground text-sm font-medium", children: config.label }), _jsx("span", { className: "text-muted-foreground text-xs", children: config.description }), _jsx(ServiceNames, { names: services
|
|
39
|
+
.filter((s) => s.category === id)
|
|
40
|
+
.map((s) => s.name) })] }), _jsx(Switch, { checked: locked ? true : !!draft[id], disabled: locked, onCheckedChange: (v) => setDraft((d) => ({ ...d, [id]: v })), "aria-label": config.label })] }, id));
|
|
41
|
+
}) }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [mode === "opt-out" ? (_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground text-xs underline underline-offset-2", onClick: () => {
|
|
42
|
+
const rejected = {};
|
|
43
|
+
for (const c of categories)
|
|
44
|
+
rejected[c.id] = !!c.locked;
|
|
45
|
+
save(rejected);
|
|
46
|
+
onClose();
|
|
47
|
+
}, children: "Do not sell or share my personal information" })) : (_jsx("span", {})), _jsx(Button, { size: "sm", onClick: () => {
|
|
48
|
+
save(draft);
|
|
49
|
+
onClose();
|
|
50
|
+
}, children: "Save preferences" })] })] }) }));
|
|
51
|
+
}
|
|
52
|
+
/** Lists the registered services in a category, for transparency in the manager. */
|
|
53
|
+
function ServiceNames({ names }) {
|
|
54
|
+
if (names.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
return (_jsx("span", { className: "text-muted-foreground/70 text-xs", children: names.join(", ") }));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=consent-banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-banner.js","sourceRoot":"","sources":["../src/consent-banner.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { ConsentProviderProps, ConsentValue, ServiceDefinition } from "./types";
|
|
3
|
+
/** Internal context value: the public surface plus what the banner/registry need. */
|
|
4
|
+
type InternalConsentValue = ConsentValue & {
|
|
5
|
+
services: ServiceDefinition[];
|
|
6
|
+
cookieless: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Internal — banner + service registry read the full value. */
|
|
9
|
+
export declare function useConsentContext(): InternalConsentValue;
|
|
10
|
+
/** Public hook: the consent matrix and the actions to change it. */
|
|
11
|
+
export declare function useConsent(): ConsentValue;
|
|
12
|
+
export declare function ConsentProvider({ country, region, categories: categoriesInput, services, forceBanner, cookieless, cookieName, onChange, children, }: ConsentProviderProps): React.JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=consent-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-provider.d.ts","sourceRoot":"","sources":["../src/consent-provider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,OAAO,KAAK,EAIV,oBAAoB,EAEpB,YAAY,EACZ,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAqGjB,qFAAqF;AACrF,KAAK,oBAAoB,GAAG,YAAY,GAAG;IACzC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAIF,gEAAgE;AAChE,wBAAgB,iBAAiB,IAAI,oBAAoB,CAMxD;AAED,oEAAoE;AACpE,wBAAgB,UAAU,IAAI,YAAY,CAEzC;AAED,wBAAgB,eAAe,CAAC,EAC9B,OAAc,EACd,MAAa,EACb,UAAU,EAAE,eAAe,EAC3B,QAAa,EACb,WAAmB,EACnB,UAAkB,EAClB,UAA8B,EAC9B,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,qBA6FtB"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ConsentBanner } from "./consent-banner";
|
|
5
|
+
import { consentMode } from "./regions";
|
|
6
|
+
import { ConsentedServices } from "./service-registry";
|
|
7
|
+
/**
|
|
8
|
+
* Region-aware, analytics-agnostic consent. Holds the per-category consent
|
|
9
|
+
* matrix, persists it to a first-party cookie, derives the consent *mode* from
|
|
10
|
+
* the visitor's region (passed in via props — no geo fetch), and decides whether
|
|
11
|
+
* the banner shows. Mount it once near the root of the app; it renders the
|
|
12
|
+
* banner and the consent-gated services itself, so the layout wires one tag.
|
|
13
|
+
* `useConsent()` exposes the matrix to any client component, and every change
|
|
14
|
+
* dispatches a `caprail:consent` window event for non-React listeners.
|
|
15
|
+
*/
|
|
16
|
+
const CONSENT_EVENT = "caprail:consent";
|
|
17
|
+
/** Six months — long enough that a returning visitor isn't re-prompted. */
|
|
18
|
+
const CONSENT_MAX_AGE = 60 * 60 * 24 * 182;
|
|
19
|
+
const DEFAULT_CATEGORIES = [
|
|
20
|
+
{
|
|
21
|
+
id: "necessary",
|
|
22
|
+
label: "Necessary",
|
|
23
|
+
description: "Required for the site to work. Always on.",
|
|
24
|
+
locked: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "analytics",
|
|
28
|
+
label: "Analytics",
|
|
29
|
+
description: "Helps us understand how the site is used.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "marketing",
|
|
33
|
+
label: "Marketing",
|
|
34
|
+
description: "Used to measure and tailor promotions.",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
/** Normalize the category list: a locked `necessary` entry always leads. */
|
|
38
|
+
function normalizeCategories(input) {
|
|
39
|
+
const provided = input?.length ? input : DEFAULT_CATEGORIES;
|
|
40
|
+
const necessary = provided.find((c) => c.id === "necessary");
|
|
41
|
+
const rest = provided.filter((c) => c.id !== "necessary");
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
id: "necessary",
|
|
45
|
+
label: necessary?.label ?? "Necessary",
|
|
46
|
+
description: necessary?.description ?? "Required for the site to work. Always on.",
|
|
47
|
+
locked: true,
|
|
48
|
+
},
|
|
49
|
+
...rest.map((c) => ({ ...c, locked: c.locked ?? false })),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
/** Defaults before a choice: locked stays on, opt-in blocks the rest, others allow. */
|
|
53
|
+
function defaultsFor(mode, categories) {
|
|
54
|
+
const allowed = mode !== "opt-in";
|
|
55
|
+
const state = {};
|
|
56
|
+
for (const c of categories)
|
|
57
|
+
state[c.id] = c.locked ? true : allowed;
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
/** All non-essential off — the privacy-safe seed before a decision settles. */
|
|
61
|
+
function blocked(categories) {
|
|
62
|
+
const state = {};
|
|
63
|
+
for (const c of categories)
|
|
64
|
+
state[c.id] = !!c.locked;
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
function readConsent(cookieName, categories) {
|
|
68
|
+
if (typeof document === "undefined")
|
|
69
|
+
return null;
|
|
70
|
+
for (const part of document.cookie.split(";")) {
|
|
71
|
+
const idx = part.indexOf("=");
|
|
72
|
+
if (idx === -1)
|
|
73
|
+
continue;
|
|
74
|
+
if (part.slice(0, idx).trim() !== cookieName)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(decodeURIComponent(part.slice(idx + 1).trim()));
|
|
78
|
+
const state = {};
|
|
79
|
+
for (const c of categories) {
|
|
80
|
+
state[c.id] = c.locked ? true : parsed[c.id] === true;
|
|
81
|
+
}
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function writeConsent(cookieName, state) {
|
|
91
|
+
document.cookie = `${cookieName}=${encodeURIComponent(JSON.stringify(state))}; Path=/; Max-Age=${CONSENT_MAX_AGE}; SameSite=Lax`;
|
|
92
|
+
}
|
|
93
|
+
const ConsentContext = React.createContext(null);
|
|
94
|
+
/** Internal — banner + service registry read the full value. */
|
|
95
|
+
export function useConsentContext() {
|
|
96
|
+
const ctx = React.useContext(ConsentContext);
|
|
97
|
+
if (!ctx) {
|
|
98
|
+
throw new Error("useConsent must be used within a ConsentProvider");
|
|
99
|
+
}
|
|
100
|
+
return ctx;
|
|
101
|
+
}
|
|
102
|
+
/** Public hook: the consent matrix and the actions to change it. */
|
|
103
|
+
export function useConsent() {
|
|
104
|
+
return useConsentContext();
|
|
105
|
+
}
|
|
106
|
+
export function ConsentProvider({ country = null, region = null, categories: categoriesInput, services = [], forceBanner = false, cookieless = false, cookieName = "caprail_consent", onChange, children, }) {
|
|
107
|
+
const categories = React.useMemo(() => normalizeCategories(categoriesInput), [categoriesInput]);
|
|
108
|
+
const mode = consentMode(country, region);
|
|
109
|
+
// The toggles shown (and what makes a banner "needed"): every configured
|
|
110
|
+
// non-locked category. Services gate *loading*; the category config drives the
|
|
111
|
+
// surface, so a consumer who lists a category gets a toggle for it.
|
|
112
|
+
const categoriesInUse = React.useMemo(() => categories.filter((c) => !c.locked).map((c) => c.id), [categories]);
|
|
113
|
+
const bannerNeeded = forceBanner || categoriesInUse.length > 0;
|
|
114
|
+
// `mounted` gates client-only reads so SSR and the first client render agree —
|
|
115
|
+
// the banner never flashes during hydration.
|
|
116
|
+
const [mounted, setMounted] = React.useState(false);
|
|
117
|
+
const [decided, setDecided] = React.useState(false);
|
|
118
|
+
// Until mounted we keep non-essential OFF (privacy-safe) so an opt-in visitor
|
|
119
|
+
// never has a service load before the stored decision is read.
|
|
120
|
+
const [consent, setConsent] = React.useState(() => bannerNeeded ? blocked(categories) : defaultsFor("notice", categories));
|
|
121
|
+
// One-time mount: a stored decision wins; otherwise seed this region's
|
|
122
|
+
// defaults. This genuinely syncs with an external system (the consent cookie)
|
|
123
|
+
// and is the hydration-safe mount pattern.
|
|
124
|
+
/* eslint-disable react-hooks/set-state-in-effect */
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
const stored = readConsent(cookieName, categories);
|
|
127
|
+
if (stored) {
|
|
128
|
+
setConsent(stored);
|
|
129
|
+
setDecided(true);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
setConsent(defaultsFor(mode, categories));
|
|
133
|
+
}
|
|
134
|
+
setMounted(true);
|
|
135
|
+
// Re-seed if the cookie name, category set, or region mode changes.
|
|
136
|
+
}, [cookieName, categories, mode]);
|
|
137
|
+
/* eslint-enable react-hooks/set-state-in-effect */
|
|
138
|
+
// Broadcast every settled change so non-React listeners (and injected SDKs)
|
|
139
|
+
// can react without prop-drilling. Skipped until mounted to avoid SSR noise.
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
if (!mounted || typeof window === "undefined")
|
|
142
|
+
return;
|
|
143
|
+
window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: consent }));
|
|
144
|
+
}, [mounted, consent]);
|
|
145
|
+
const commit = React.useCallback((next) => {
|
|
146
|
+
const full = { ...next };
|
|
147
|
+
for (const c of categories)
|
|
148
|
+
if (c.locked)
|
|
149
|
+
full[c.id] = true;
|
|
150
|
+
writeConsent(cookieName, full);
|
|
151
|
+
setConsent(full);
|
|
152
|
+
setDecided(true);
|
|
153
|
+
onChange?.(full);
|
|
154
|
+
}, [cookieName, categories, onChange]);
|
|
155
|
+
const showBanner = mounted && bannerNeeded && !decided;
|
|
156
|
+
const value = {
|
|
157
|
+
consent,
|
|
158
|
+
mode,
|
|
159
|
+
region: country,
|
|
160
|
+
categories,
|
|
161
|
+
categoriesInUse,
|
|
162
|
+
showBanner,
|
|
163
|
+
services,
|
|
164
|
+
cookieless,
|
|
165
|
+
acceptAll: () => {
|
|
166
|
+
const next = {};
|
|
167
|
+
for (const c of categories)
|
|
168
|
+
next[c.id] = true;
|
|
169
|
+
commit(next);
|
|
170
|
+
},
|
|
171
|
+
rejectAll: () => commit(blocked(categories)),
|
|
172
|
+
setCategory: (category, val) => commit({ ...consent, [category]: val }),
|
|
173
|
+
save: (next) => commit(next),
|
|
174
|
+
openBanner: () => setDecided(false),
|
|
175
|
+
};
|
|
176
|
+
return (_jsxs(ConsentContext.Provider, { value: value, children: [children, _jsx(ConsentBanner, {}), _jsx(ConsentedServices, {})] }));
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=consent-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-provider.js","sourceRoot":"","sources":["../src/consent-provider.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ConsentProvider, useConsent } from "./consent-provider";
|
|
2
|
+
export { ConsentBanner } from "./consent-banner";
|
|
3
|
+
export { ConsentedServices } from "./service-registry";
|
|
4
|
+
export { consentMode } from "./regions";
|
|
5
|
+
export type { CategoryConfig, ConsentCategory, ConsentMode, ConsentProviderProps, ConsentState, ConsentValue, ServiceDefinition, } from "./types";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EACV,cAAc,EACd,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACZ,iBAAiB,GAClB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure region → consent-mode mapping. Kept free of React/DOM so the country
|
|
3
|
+
* logic is unit-testable in isolation and reusable on a server edge.
|
|
4
|
+
*/
|
|
5
|
+
import type { ConsentMode } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Map a geo country (+ subdivision) to a consent mode:
|
|
8
|
+
* - `opt-in` — blocked until accept (EEA + UK + CH + BR).
|
|
9
|
+
* - `opt-out` — allowed until reject, with a "Do Not Sell/Share" affordance (US-CA).
|
|
10
|
+
* - `notice` — allowed; the banner is informational (everywhere else, incl. unknown).
|
|
11
|
+
*
|
|
12
|
+
* Country/region are typically the host's edge headers (e.g. Vercel's
|
|
13
|
+
* `x-vercel-ip-country` / `x-vercel-ip-country-region`). With no country the
|
|
14
|
+
* mode is `notice`, so a site that passes nothing still shows an informational
|
|
15
|
+
* banner.
|
|
16
|
+
*/
|
|
17
|
+
export declare function consentMode(country: string | null | undefined, region: string | null | undefined): ConsentMode;
|
|
18
|
+
//# sourceMappingURL=regions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regions.d.ts","sourceRoot":"","sources":["../src/regions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAsC3C;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,WAAW,CAOb"}
|
package/dist/regions.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure region → consent-mode mapping. Kept free of React/DOM so the country
|
|
3
|
+
* logic is unit-testable in isolation and reusable on a server edge.
|
|
4
|
+
*/
|
|
5
|
+
/** EEA member states — the core GDPR footprint. */
|
|
6
|
+
const EEA = [
|
|
7
|
+
"AT",
|
|
8
|
+
"BE",
|
|
9
|
+
"BG",
|
|
10
|
+
"HR",
|
|
11
|
+
"CY",
|
|
12
|
+
"CZ",
|
|
13
|
+
"DK",
|
|
14
|
+
"EE",
|
|
15
|
+
"FI",
|
|
16
|
+
"FR",
|
|
17
|
+
"DE",
|
|
18
|
+
"GR",
|
|
19
|
+
"HU",
|
|
20
|
+
"IE",
|
|
21
|
+
"IT",
|
|
22
|
+
"LV",
|
|
23
|
+
"LT",
|
|
24
|
+
"LU",
|
|
25
|
+
"MT",
|
|
26
|
+
"NL",
|
|
27
|
+
"PL",
|
|
28
|
+
"PT",
|
|
29
|
+
"RO",
|
|
30
|
+
"SK",
|
|
31
|
+
"SI",
|
|
32
|
+
"ES",
|
|
33
|
+
"SE",
|
|
34
|
+
"IS",
|
|
35
|
+
"LI",
|
|
36
|
+
"NO",
|
|
37
|
+
];
|
|
38
|
+
/** Opt-in (consent-before-tracking) regions: EEA + UK + Switzerland + Brazil. */
|
|
39
|
+
const OPT_IN = new Set([...EEA, "GB", "CH", "BR"]);
|
|
40
|
+
/**
|
|
41
|
+
* Map a geo country (+ subdivision) to a consent mode:
|
|
42
|
+
* - `opt-in` — blocked until accept (EEA + UK + CH + BR).
|
|
43
|
+
* - `opt-out` — allowed until reject, with a "Do Not Sell/Share" affordance (US-CA).
|
|
44
|
+
* - `notice` — allowed; the banner is informational (everywhere else, incl. unknown).
|
|
45
|
+
*
|
|
46
|
+
* Country/region are typically the host's edge headers (e.g. Vercel's
|
|
47
|
+
* `x-vercel-ip-country` / `x-vercel-ip-country-region`). With no country the
|
|
48
|
+
* mode is `notice`, so a site that passes nothing still shows an informational
|
|
49
|
+
* banner.
|
|
50
|
+
*/
|
|
51
|
+
export function consentMode(country, region) {
|
|
52
|
+
if (!country)
|
|
53
|
+
return "notice";
|
|
54
|
+
const c = country.toUpperCase();
|
|
55
|
+
if (OPT_IN.has(c))
|
|
56
|
+
return "opt-in";
|
|
57
|
+
// CCPA/CPRA — opt-out applies to California specifically.
|
|
58
|
+
if (c === "US" && region?.toUpperCase() === "CA")
|
|
59
|
+
return "opt-out";
|
|
60
|
+
return "notice";
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=regions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regions.js","sourceRoot":"","sources":["../src/regions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,mDAAmD;AACnD,MAAM,GAAG,GAAG;IACV,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;CACL,CAAC;AACF,iFAAiF;AACjF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CACzB,OAAkC,EAClC,MAAiC;IAEjC,IAAI,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAChC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAAE,OAAO,QAAQ,CAAC;IACnC,0DAA0D;IAC1D,IAAI,CAAC,KAAK,IAAI,IAAI,MAAM,EAAE,WAAW,EAAE,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACnE,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Third-party service injection. Services are passed to `ConsentProvider` via
|
|
4
|
+
* the `services` prop — nothing is bundled. Each entry declares the consent
|
|
5
|
+
* `category` that gates it and a `Loader` (typically `next/script` tags) mounted
|
|
6
|
+
* ONLY while that category is granted; unmounting on revoke lets a service tear
|
|
7
|
+
* down. `cookieless` is forwarded from the provider so a loader can run its SDK
|
|
8
|
+
* in a memory-only mode.
|
|
9
|
+
*
|
|
10
|
+
* The provider already renders this once inside its subtree, so consumers don't
|
|
11
|
+
* mount it themselves.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ConsentedServices(): React.JSX.Element;
|
|
14
|
+
//# sourceMappingURL=service-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-registry.d.ts","sourceRoot":"","sources":["../src/service-registry.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,sBAWhC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useConsentContext } from "./consent-provider";
|
|
5
|
+
/**
|
|
6
|
+
* Third-party service injection. Services are passed to `ConsentProvider` via
|
|
7
|
+
* the `services` prop — nothing is bundled. Each entry declares the consent
|
|
8
|
+
* `category` that gates it and a `Loader` (typically `next/script` tags) mounted
|
|
9
|
+
* ONLY while that category is granted; unmounting on revoke lets a service tear
|
|
10
|
+
* down. `cookieless` is forwarded from the provider so a loader can run its SDK
|
|
11
|
+
* in a memory-only mode.
|
|
12
|
+
*
|
|
13
|
+
* The provider already renders this once inside its subtree, so consumers don't
|
|
14
|
+
* mount it themselves.
|
|
15
|
+
*/
|
|
16
|
+
export function ConsentedServices() {
|
|
17
|
+
const { services, consent, cookieless } = useConsentContext();
|
|
18
|
+
return (_jsx(_Fragment, { children: services
|
|
19
|
+
.filter((s) => consent[s.category])
|
|
20
|
+
.map((s) => (_jsx(s.Loader, { cookieless: cookieless }, s.id))) }));
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=service-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-registry.js","sourceRoot":"","sources":["../src/service-registry.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAC9D,OAAO,CACL,4BACG,QAAQ;aACN,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;aAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACV,KAAC,CAAC,CAAC,MAAM,IAAY,UAAU,EAAE,UAAU,IAA5B,CAAC,CAAC,EAAE,CAA4B,CAChD,CAAC,GACH,CACJ,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Public types for `@caprail-dev/consent`. Kept React/DOM-light so they can be
|
|
4
|
+
* imported on a server edge (e.g. to compute a `consentMode` in middleware).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A consent category id. The three defaults are `necessary` / `analytics` /
|
|
8
|
+
* `marketing`, but a consumer can pass arbitrary ids via `categories`, so this
|
|
9
|
+
* is a plain string rather than a closed union.
|
|
10
|
+
*/
|
|
11
|
+
export type ConsentCategory = string;
|
|
12
|
+
/** The consent matrix: per-category granted flags. `necessary` is always true. */
|
|
13
|
+
export type ConsentState = Record<ConsentCategory, boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* How non-essential categories are treated before the visitor chooses:
|
|
16
|
+
* - `opt-in` — blocked until accept.
|
|
17
|
+
* - `opt-out` — allowed until reject (shows a "Do Not Sell/Share" affordance).
|
|
18
|
+
* - `notice` — allowed; the banner is informational.
|
|
19
|
+
*/
|
|
20
|
+
export type ConsentMode = "opt-in" | "opt-out" | "notice";
|
|
21
|
+
/**
|
|
22
|
+
* A consent category shown in the banner's "manage" view. `locked` categories
|
|
23
|
+
* (e.g. `necessary`) are always granted and can't be toggled off.
|
|
24
|
+
*/
|
|
25
|
+
export type CategoryConfig = {
|
|
26
|
+
/** Stable id used as the key in `ConsentState`, e.g. "analytics". */
|
|
27
|
+
id: ConsentCategory;
|
|
28
|
+
/** Human label shown in the manage view, e.g. "Analytics". */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Short explanation shown under the label. */
|
|
31
|
+
description: string;
|
|
32
|
+
/** Always-on and non-toggleable. Defaults to true only for `necessary`. */
|
|
33
|
+
locked?: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A third-party service gated by a consent category. Nothing is bundled — a
|
|
37
|
+
* consumer registers GA / PostHog / etc. by passing entries whose `Loader`
|
|
38
|
+
* renders the tool's tags (e.g. a `next/script` snippet).
|
|
39
|
+
*/
|
|
40
|
+
export type ServiceDefinition = {
|
|
41
|
+
/** Stable id, e.g. "posthog" / "google-analytics". */
|
|
42
|
+
id: string;
|
|
43
|
+
/** Human label shown in the banner's "manage" view, for transparency. */
|
|
44
|
+
name: string;
|
|
45
|
+
/** Consent category that gates this service. `necessary` services always load. */
|
|
46
|
+
category: ConsentCategory;
|
|
47
|
+
/**
|
|
48
|
+
* Renders the actual tags. Mounted only while the gating category is granted;
|
|
49
|
+
* unmounting on revoke lets a service tear down. `cookieless` is forwarded
|
|
50
|
+
* from the provider's `cookieless` prop so a loader can run its SDK in a
|
|
51
|
+
* memory-only / no-cookie mode (e.g. PostHog `persistence: "memory"`).
|
|
52
|
+
*/
|
|
53
|
+
Loader: React.ComponentType<{
|
|
54
|
+
cookieless: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
export type ConsentProviderProps = {
|
|
58
|
+
/**
|
|
59
|
+
* Visitor country code (ISO-3166-1 alpha-2) from the host's edge, e.g.
|
|
60
|
+
* `x-vercel-ip-country`. Optional — with no country the mode is `notice`.
|
|
61
|
+
*/
|
|
62
|
+
country?: string | null;
|
|
63
|
+
/** Visitor subdivision (e.g. `x-vercel-ip-country-region`) — used for US-CA. */
|
|
64
|
+
region?: string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Categories offered in the banner. Defaults to necessary / analytics /
|
|
67
|
+
* marketing. Order is preserved; a `necessary` (locked) category is always
|
|
68
|
+
* present even if omitted.
|
|
69
|
+
*/
|
|
70
|
+
categories?: CategoryConfig[];
|
|
71
|
+
/** Third-party services to gate + list. Empty by default — nothing is bundled. */
|
|
72
|
+
services?: ServiceDefinition[];
|
|
73
|
+
/**
|
|
74
|
+
* Show the banner even when no non-essential category or service is in use
|
|
75
|
+
* (e.g. a site that wants a standing cookie notice). Defaults to false.
|
|
76
|
+
*/
|
|
77
|
+
forceBanner?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Run gated services in a cookieless / memory-only mode. Forwarded to each
|
|
80
|
+
* `Loader` as `cookieless`. Defaults to false.
|
|
81
|
+
*/
|
|
82
|
+
cookieless?: boolean;
|
|
83
|
+
/** First-party cookie name the decision is stored under. Defaults `caprail_consent`. */
|
|
84
|
+
cookieName?: string;
|
|
85
|
+
/** Called after every settled change (accept / reject / save). */
|
|
86
|
+
onChange?: (state: ConsentState) => void;
|
|
87
|
+
children: React.ReactNode;
|
|
88
|
+
};
|
|
89
|
+
/** The value exposed by `useConsent()`. */
|
|
90
|
+
export type ConsentValue = {
|
|
91
|
+
/** The current consent matrix. */
|
|
92
|
+
consent: ConsentState;
|
|
93
|
+
/** Derived mode for the visitor's region. */
|
|
94
|
+
mode: ConsentMode;
|
|
95
|
+
/** Visitor country code, or null when unknown. */
|
|
96
|
+
region: string | null;
|
|
97
|
+
/** Configured categories (necessary first). */
|
|
98
|
+
categories: CategoryConfig[];
|
|
99
|
+
/** Non-essential categories actually gating something (drives which toggles show). */
|
|
100
|
+
categoriesInUse: ConsentCategory[];
|
|
101
|
+
/** Whether the banner should currently render. */
|
|
102
|
+
showBanner: boolean;
|
|
103
|
+
acceptAll: () => void;
|
|
104
|
+
rejectAll: () => void;
|
|
105
|
+
setCategory: (category: ConsentCategory, value: boolean) => void;
|
|
106
|
+
/** Persist a full category state at once (the manage view's Save). */
|
|
107
|
+
save: (state: ConsentState) => void;
|
|
108
|
+
/** Re-open the banner after a decision (e.g. a footer "Cookie settings" link). */
|
|
109
|
+
openBanner: () => void;
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC;;;GAGG;AAEH;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC,kFAAkF;AAClF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,qEAAqE;IACrE,EAAE,EAAE,eAAe,CAAC;IACpB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC/B;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,2CAA2C;AAC3C,MAAM,MAAM,YAAY,GAAG;IACzB,kCAAkC;IAClC,OAAO,EAAE,YAAY,CAAC;IACtB,6CAA6C;IAC7C,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,sFAAsF;IACtF,eAAe,EAAE,eAAe,EAAE,CAAC;IACnC,kDAAkD;IAClD,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACjE,sEAAsE;IACtE,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACpC,kFAAkF;IAClF,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type ButtonVariant = "default" | "outline" | "ghost";
|
|
3
|
+
type ButtonSize = "default" | "sm" | "icon";
|
|
4
|
+
export type ButtonProps = React.ComponentProps<"button"> & {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
size?: ButtonSize;
|
|
7
|
+
};
|
|
8
|
+
declare function Button({ className, variant, size, ...props }: ButtonProps): React.JSX.Element;
|
|
9
|
+
export { Button };
|
|
10
|
+
//# sourceMappingURL=button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../src/ui/button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AACrD,KAAK,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAe5C,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG;IACzD,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAmB,EACnB,IAAgB,EAChB,GAAG,KAAK,EACT,EAAE,WAAW,qBAab;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../cn";
|
|
5
|
+
const VARIANT_CLASS = {
|
|
6
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
7
|
+
outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
8
|
+
ghost: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
9
|
+
};
|
|
10
|
+
const SIZE_CLASS = {
|
|
11
|
+
default: "h-9 px-4 text-sm",
|
|
12
|
+
sm: "h-8 px-3 text-[0.8rem]",
|
|
13
|
+
icon: "size-9",
|
|
14
|
+
};
|
|
15
|
+
function Button({ className, variant = "default", size = "default", ...props }) {
|
|
16
|
+
return (_jsx("button", { "data-slot": "button", className: cn("focus-visible:ring-ring inline-flex items-center justify-center gap-2 rounded-lg font-medium whitespace-nowrap transition-colors outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50", VARIANT_CLASS[variant], SIZE_CLASS[size], className), ...props }));
|
|
17
|
+
}
|
|
18
|
+
export { Button };
|
|
19
|
+
//# sourceMappingURL=button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../../src/ui/button.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAS3B,MAAM,aAAa,GAAkC;IACnD,OAAO,EAAE,wDAAwD;IACjE,OAAO,EACL,gGAAgG;IAClG,KAAK,EAAE,oEAAoE;CAC5E,CAAC;AAEF,MAAM,UAAU,GAA+B;IAC7C,OAAO,EAAE,kBAAkB;IAC3B,EAAE,EAAE,wBAAwB;IAC5B,IAAI,EAAE,QAAQ;CACf,CAAC;AAOF,SAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,GAAG,SAAS,EACnB,IAAI,GAAG,SAAS,EAChB,GAAG,KAAK,EACI;IACZ,OAAO,CACL,8BACY,QAAQ,EAClB,SAAS,EAAE,EAAE,CACX,qNAAqN,EACrN,aAAa,CAAC,OAAO,CAAC,EACtB,UAAU,CAAC,IAAI,CAAC,EAChB,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
3
|
+
declare function Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>): React.JSX.Element;
|
|
4
|
+
declare function DialogContent({ className, children, showClose, ...props }: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
5
|
+
showClose?: boolean;
|
|
6
|
+
}): React.JSX.Element;
|
|
7
|
+
declare function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
8
|
+
declare function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>): React.JSX.Element;
|
|
9
|
+
declare function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>): React.JSX.Element;
|
|
10
|
+
export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription };
|
|
11
|
+
//# sourceMappingURL=dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../../src/ui/dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAC;AAU1D,iBAAS,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBAEvE;AAkBD,iBAAS,aAAa,CAAC,EACrB,SAAS,EACT,QAAQ,EACR,SAAgB,EAChB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,GAAG;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,qBAmCA;AAED,iBAAS,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQzE;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAQpD;AAED,iBAAS,iBAAiB,CAAC,EACzB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,WAAW,CAAC,qBAQ1D;AAED,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { cn } from "../cn";
|
|
6
|
+
// Shadcn-style Dialog on STANDARD tokens. Content is portaled to <body> and
|
|
7
|
+
// themed with `--background` / `--border` / `--foreground` so it follows the
|
|
8
|
+
// consumer's theme. Kept dependency-free: the close glyph is an inline SVG (no
|
|
9
|
+
// icon library) and there are no animation-plugin classes, only Tailwind
|
|
10
|
+
// transitions Radix toggles via `data-state`.
|
|
11
|
+
function Dialog(props) {
|
|
12
|
+
return _jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
|
|
13
|
+
}
|
|
14
|
+
function DialogOverlay({ className, ...props }) {
|
|
15
|
+
return (_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity data-[state=closed]:opacity-0 motion-reduce:transition-none", className), ...props }));
|
|
16
|
+
}
|
|
17
|
+
function DialogContent({ className, children, showClose = true, ...props }) {
|
|
18
|
+
return (_jsxs(DialogPrimitive.Portal, { "data-slot": "dialog-portal", children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn("border-border bg-background text-foreground fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl border p-5 shadow-xl", className), ...props, children: [children, showClose && (_jsx(DialogPrimitive.Close, { "aria-label": "Close", className: "text-muted-foreground focus-visible:ring-ring absolute top-4 right-4 grid size-8 place-items-center rounded-lg transition-opacity hover:opacity-70 focus:outline-none focus-visible:ring-2", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "size-5", "aria-hidden": "true", children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) }))] })] }));
|
|
19
|
+
}
|
|
20
|
+
function DialogHeader({ className, ...props }) {
|
|
21
|
+
return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-1.5", className), ...props }));
|
|
22
|
+
}
|
|
23
|
+
function DialogTitle({ className, ...props }) {
|
|
24
|
+
return (_jsx(DialogPrimitive.Title, { "data-slot": "dialog-title", className: cn("text-foreground text-base font-semibold", className), ...props }));
|
|
25
|
+
}
|
|
26
|
+
function DialogDescription({ className, ...props }) {
|
|
27
|
+
return (_jsx(DialogPrimitive.Description, { "data-slot": "dialog-description", className: cn("text-muted-foreground text-sm", className), ...props }));
|
|
28
|
+
}
|
|
29
|
+
export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription };
|
|
30
|
+
//# sourceMappingURL=dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../src/ui/dialog.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAE3B,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,yEAAyE;AACzE,8CAA8C;AAE9C,SAAS,MAAM,CAAC,KAAwD;IACtE,OAAO,KAAC,eAAe,CAAC,IAAI,iBAAW,QAAQ,KAAK,KAAK,GAAI,CAAC;AAChE,CAAC;AAED,SAAS,aAAa,CAAC,EACrB,SAAS,EACT,GAAG,KAAK,EAC6C;IACrD,OAAO,CACL,KAAC,eAAe,CAAC,OAAO,iBACZ,gBAAgB,EAC1B,SAAS,EAAE,EAAE,CACX,+GAA+G,EAC/G,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,EACrB,SAAS,EACT,QAAQ,EACR,SAAS,GAAG,IAAI,EAChB,GAAG,KAAK,EAGT;IACC,OAAO,CACL,MAAC,eAAe,CAAC,MAAM,iBAAW,eAAe,aAC/C,KAAC,aAAa,KAAG,EACjB,MAAC,eAAe,CAAC,OAAO,iBACZ,gBAAgB,EAC1B,SAAS,EAAE,EAAE,CACX,oLAAoL,EACpL,SAAS,CACV,KACG,KAAK,aAER,QAAQ,EACR,SAAS,IAAI,CACZ,KAAC,eAAe,CAAC,KAAK,kBACT,OAAO,EAClB,SAAS,EAAC,4LAA4L,YAEtM,cACE,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,SAAS,EAAC,QAAQ,iBACN,MAAM,YAElB,eAAM,CAAC,EAAC,sBAAsB,GAAG,GAC7B,GACgB,CACzB,IACuB,IACH,CAC1B,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACxE,OAAO,CACL,2BACY,eAAe,EACzB,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,KAC7C,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EAC2C;IACnD,OAAO,CACL,KAAC,eAAe,CAAC,KAAK,iBACV,cAAc,EACxB,SAAS,EAAE,EAAE,CAAC,yCAAyC,EAAE,SAAS,CAAC,KAC/D,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,EACzB,SAAS,EACT,GAAG,KAAK,EACiD;IACzD,OAAO,CACL,KAAC,eAAe,CAAC,WAAW,iBAChB,oBAAoB,EAC9B,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KACrD,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
3
|
+
declare function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>): React.JSX.Element;
|
|
4
|
+
export { Switch };
|
|
5
|
+
//# sourceMappingURL=switch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../src/ui/switch.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAC;AAS1D,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBAgBnD;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
5
|
+
import { cn } from "../cn";
|
|
6
|
+
// Shadcn-style Switch on STANDARD tokens: checked track is `--primary`,
|
|
7
|
+
// unchecked is `--input`, thumb is `--background`. The data-state colors are
|
|
8
|
+
// applied via Tailwind's `data-[state=…]` variants so a consumer can still
|
|
9
|
+
// override with a `className`.
|
|
10
|
+
function Switch({ className, ...props }) {
|
|
11
|
+
return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn("peer focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "bg-background pointer-events-none block size-4 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[1.125rem]" }) }));
|
|
12
|
+
}
|
|
13
|
+
export { Switch };
|
|
14
|
+
//# sourceMappingURL=switch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"switch.js","sourceRoot":"","sources":["../../src/ui/switch.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAE3B,wEAAwE;AACxE,6EAA6E;AAC7E,2EAA2E;AAC3E,+BAA+B;AAE/B,SAAS,MAAM,CAAC,EACd,SAAS,EACT,GAAG,KAAK,EAC0C;IAClD,OAAO,CACL,KAAC,eAAe,CAAC,IAAI,iBACT,QAAQ,EAClB,SAAS,EAAE,EAAE,CACX,kSAAkS,EAClS,SAAS,CACV,KACG,KAAK,YAET,KAAC,eAAe,CAAC,KAAK,iBACV,cAAc,EACxB,SAAS,EAAC,wJAAwJ,GAClK,GACmB,CACxB,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@caprail-dev/consent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Region-aware, analytics-agnostic cookie-consent banner and state for React — Shadcn-styled, Tailwind-themeable.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"prepublishOnly": "bun run build",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@radix-ui/react-dialog": "^1.1.17",
|
|
30
|
+
"@radix-ui/react-switch": "^1.3.1",
|
|
31
|
+
"clsx": "^2.1.1",
|
|
32
|
+
"tailwind-merge": "^3.6.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-dom": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"react": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"react-dom": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/react": "^19.2.16"
|
|
48
|
+
}
|
|
49
|
+
}
|