crumbie 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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/cookie-QMHHGW4Q.png +0 -0
- package/dist/cookie-settings.module-DU4XK62C.module.css +26 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CookieKit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Crumbie
|
|
2
|
+
|
|
3
|
+
**Cookie consent components you'll actually want on your website.**
|
|
4
|
+
|
|
5
|
+
Beautiful, accessible, and fully editable cookie consent for modern React applications. Crumbie lets you own the code with no iframes, third-party widgets, or backend.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Cookie banner with Accept / Decline / Settings
|
|
10
|
+
- Cookie settings modal (Necessary, Preferences, Analytics, Marketing)
|
|
11
|
+
- Floating settings button
|
|
12
|
+
- `ConsentGate` for conditional rendering
|
|
13
|
+
- `useCookieConsent` and `useConsentCategory` hooks
|
|
14
|
+
- localStorage persistence with versioning and expiration
|
|
15
|
+
- Light & dark mode via CSS variables
|
|
16
|
+
- Accessible (keyboard, focus trap, reduced motion)
|
|
17
|
+
- TypeScript-first, SSR-safe, zero backend
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm install
|
|
23
|
+
pnpm dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import {
|
|
28
|
+
CrumbieProvider,
|
|
29
|
+
CookieBanner,
|
|
30
|
+
CookieSettings,
|
|
31
|
+
CookieSettingsButton,
|
|
32
|
+
} from "crumbie";
|
|
33
|
+
|
|
34
|
+
export default function RootLayout({ children }) {
|
|
35
|
+
return (
|
|
36
|
+
<CrumbieProvider>
|
|
37
|
+
{children}
|
|
38
|
+
<CookieBanner />
|
|
39
|
+
<CookieSettings />
|
|
40
|
+
<CookieSettingsButton />
|
|
41
|
+
</CrumbieProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
const {
|
|
50
|
+
consent,
|
|
51
|
+
hasConsent,
|
|
52
|
+
acceptAll,
|
|
53
|
+
declineAll,
|
|
54
|
+
updatePreferences,
|
|
55
|
+
resetConsent,
|
|
56
|
+
openSettings,
|
|
57
|
+
closeSettings,
|
|
58
|
+
isAllowed,
|
|
59
|
+
} = useCookieConsent();
|
|
60
|
+
|
|
61
|
+
const analyticsEnabled = useConsentCategory("analytics");
|
|
62
|
+
|
|
63
|
+
<ConsentGate category="analytics">
|
|
64
|
+
<Analytics />
|
|
65
|
+
</ConsentGate>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Scripts
|
|
69
|
+
|
|
70
|
+
| Command | Description |
|
|
71
|
+
| -------------- | ------------------------ |
|
|
72
|
+
| `pnpm dev` | Start Next.js dev server |
|
|
73
|
+
| `pnpm build` | Production build |
|
|
74
|
+
| `pnpm test` | Run Vitest suite |
|
|
75
|
+
| `pnpm lint` | ESLint |
|
|
76
|
+
| `pnpm format` | Prettier |
|
|
77
|
+
|
|
78
|
+
## Project structure
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
app/ # Marketing site + docs
|
|
82
|
+
components/
|
|
83
|
+
crumbie/ # Library components (banner, settings, provider…)
|
|
84
|
+
ui/ # Shared primitives (button, dialog, switch)
|
|
85
|
+
site/ # Landing & docs chrome
|
|
86
|
+
hooks/ # useCookieConsent, useConsentCategory
|
|
87
|
+
lib/ # storage + utils
|
|
88
|
+
types/ # Consent types
|
|
89
|
+
registry/ # Reserved for future CLI
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Future CLI
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx crumbie add banner
|
|
96
|
+
npx crumbie add settings
|
|
97
|
+
npx crumbie add provider
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.scrollArea {
|
|
2
|
+
scrollbar-color: color-mix(in srgb, var(--muted-foreground) 45%, transparent)
|
|
3
|
+
transparent;
|
|
4
|
+
scrollbar-width: thin;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.scrollArea::-webkit-scrollbar {
|
|
8
|
+
width: 0.5rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.scrollArea::-webkit-scrollbar-track {
|
|
12
|
+
background: transparent;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.scrollArea::-webkit-scrollbar-thumb {
|
|
16
|
+
background: color-mix(in srgb, var(--muted-foreground) 45%, transparent);
|
|
17
|
+
border: 0.1875rem solid transparent;
|
|
18
|
+
background-clip: padding-box;
|
|
19
|
+
border-radius: 9999px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.scrollArea::-webkit-scrollbar-thumb:hover {
|
|
23
|
+
background: color-mix(in srgb, var(--muted-foreground) 65%, transparent);
|
|
24
|
+
border: 0.1875rem solid transparent;
|
|
25
|
+
background-clip: padding-box;
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type CookieCategory = "necessary" | "preferences" | "analytics" | "marketing";
|
|
5
|
+
type ConsentPreferences = {
|
|
6
|
+
necessary: true;
|
|
7
|
+
preferences: boolean;
|
|
8
|
+
analytics: boolean;
|
|
9
|
+
marketing: boolean;
|
|
10
|
+
};
|
|
11
|
+
type ConsentRecord = {
|
|
12
|
+
version: number;
|
|
13
|
+
acceptedAt: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
preferences: ConsentPreferences;
|
|
16
|
+
};
|
|
17
|
+
type ConsentConfig = {
|
|
18
|
+
version?: number;
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
expirationDays?: number;
|
|
21
|
+
defaultPreferences?: Partial<Omit<ConsentPreferences, "necessary">>;
|
|
22
|
+
};
|
|
23
|
+
type CookieCategoryMeta = {
|
|
24
|
+
id: CookieCategory;
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
required: boolean;
|
|
28
|
+
};
|
|
29
|
+
declare const COOKIE_CATEGORIES: CookieCategoryMeta[];
|
|
30
|
+
declare const DEFAULT_PREFERENCES: ConsentPreferences;
|
|
31
|
+
|
|
32
|
+
type CookieConsentContextValue = {
|
|
33
|
+
consent: ConsentRecord | null;
|
|
34
|
+
preferences: ConsentPreferences;
|
|
35
|
+
hasConsent: boolean;
|
|
36
|
+
isSettingsOpen: boolean;
|
|
37
|
+
isHydrated: boolean;
|
|
38
|
+
acceptAll: () => void;
|
|
39
|
+
declineAll: () => void;
|
|
40
|
+
updatePreferences: (preferences: Partial<ConsentPreferences>) => void;
|
|
41
|
+
resetConsent: () => void;
|
|
42
|
+
openSettings: () => void;
|
|
43
|
+
closeSettings: () => void;
|
|
44
|
+
setSettingsOpen: (open: boolean) => void;
|
|
45
|
+
isAllowed: (category: CookieCategory) => boolean;
|
|
46
|
+
};
|
|
47
|
+
type CrumbieProviderProps = {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
config?: ConsentConfig;
|
|
50
|
+
};
|
|
51
|
+
declare function CrumbieProvider({ children, config, }: CrumbieProviderProps): React.JSX.Element;
|
|
52
|
+
|
|
53
|
+
type CookieBannerProps = {
|
|
54
|
+
title?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
className?: string;
|
|
57
|
+
position?: "left" | "center" | "right" | "bottom" | "bottom-left" | "bottom-right";
|
|
58
|
+
};
|
|
59
|
+
declare function CookieBanner({ title, description, className, position, }: CookieBannerProps): React.JSX.Element;
|
|
60
|
+
|
|
61
|
+
type CookieSettingsProps = {
|
|
62
|
+
title?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
className?: string;
|
|
65
|
+
};
|
|
66
|
+
declare function CookieSettings({ title, description, className, }: CookieSettingsProps): React.JSX.Element;
|
|
67
|
+
|
|
68
|
+
type CookieSettingsButtonProps = {
|
|
69
|
+
className?: string;
|
|
70
|
+
label?: string;
|
|
71
|
+
variant?: "floating" | "inline";
|
|
72
|
+
};
|
|
73
|
+
declare function CookieSettingsButton({ className, label, variant, }: CookieSettingsButtonProps): React.JSX.Element | null;
|
|
74
|
+
|
|
75
|
+
type ConsentGateProps = {
|
|
76
|
+
category: CookieCategory;
|
|
77
|
+
children: ReactNode;
|
|
78
|
+
fallback?: ReactNode;
|
|
79
|
+
};
|
|
80
|
+
declare function ConsentGate({ category, children, fallback, }: ConsentGateProps): React.JSX.Element;
|
|
81
|
+
|
|
82
|
+
declare function useCookieConsent(): CookieConsentContextValue;
|
|
83
|
+
|
|
84
|
+
declare function useConsentCategory(category: CookieCategory): boolean;
|
|
85
|
+
|
|
86
|
+
export { COOKIE_CATEGORIES, type ConsentConfig, ConsentGate, type ConsentGateProps, type ConsentPreferences, type ConsentRecord, CookieBanner, type CookieBannerProps, type CookieCategory, type CookieCategoryMeta, type CookieConsentContextValue, CookieSettings, CookieSettingsButton, type CookieSettingsButtonProps, type CookieSettingsProps, CrumbieProvider, type CrumbieProviderProps, DEFAULT_PREFERENCES, useConsentCategory, useCookieConsent };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
var __objRest = (source, exclude) => {
|
|
21
|
+
var target = {};
|
|
22
|
+
for (var prop in source)
|
|
23
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
24
|
+
target[prop] = source[prop];
|
|
25
|
+
if (source != null && __getOwnPropSymbols)
|
|
26
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
27
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
28
|
+
target[prop] = source[prop];
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// components/crumbie/provider.tsx
|
|
34
|
+
import * as React from "react";
|
|
35
|
+
|
|
36
|
+
// types/consent.ts
|
|
37
|
+
var COOKIE_CATEGORIES = [
|
|
38
|
+
{
|
|
39
|
+
id: "necessary",
|
|
40
|
+
title: "Necessary",
|
|
41
|
+
description: "Essential cookies required for the website to function. These cannot be disabled.",
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "preferences",
|
|
46
|
+
title: "Preferences",
|
|
47
|
+
description: "Cookies that remember your settings and preferences for a more personalized experience.",
|
|
48
|
+
required: false
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "analytics",
|
|
52
|
+
title: "Analytics",
|
|
53
|
+
description: "Cookies that help us understand how visitors interact with our website.",
|
|
54
|
+
required: false
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "marketing",
|
|
58
|
+
title: "Marketing",
|
|
59
|
+
description: "Cookies used to deliver relevant ads and measure campaign performance.",
|
|
60
|
+
required: false
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
var DEFAULT_PREFERENCES = {
|
|
64
|
+
necessary: true,
|
|
65
|
+
preferences: false,
|
|
66
|
+
analytics: false,
|
|
67
|
+
marketing: false
|
|
68
|
+
};
|
|
69
|
+
var CONSENT_VERSION = 1;
|
|
70
|
+
var STORAGE_KEY = "crumbie-consent";
|
|
71
|
+
var EXPIRATION_DAYS = 365;
|
|
72
|
+
|
|
73
|
+
// lib/storage.ts
|
|
74
|
+
function isBrowser() {
|
|
75
|
+
return typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
76
|
+
}
|
|
77
|
+
function resolveConfig(config) {
|
|
78
|
+
var _a, _b, _c;
|
|
79
|
+
return {
|
|
80
|
+
version: (_a = config == null ? void 0 : config.version) != null ? _a : CONSENT_VERSION,
|
|
81
|
+
storageKey: (_b = config == null ? void 0 : config.storageKey) != null ? _b : STORAGE_KEY,
|
|
82
|
+
expirationDays: (_c = config == null ? void 0 : config.expirationDays) != null ? _c : EXPIRATION_DAYS,
|
|
83
|
+
defaultPreferences: __spreadProps(__spreadValues(__spreadValues({}, DEFAULT_PREFERENCES), config == null ? void 0 : config.defaultPreferences), {
|
|
84
|
+
necessary: true
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isValidPreferences(value) {
|
|
89
|
+
if (!value || typeof value !== "object") return false;
|
|
90
|
+
const prefs = value;
|
|
91
|
+
return prefs.necessary === true && typeof prefs.preferences === "boolean" && typeof prefs.analytics === "boolean" && typeof prefs.marketing === "boolean";
|
|
92
|
+
}
|
|
93
|
+
function isValidRecord(value) {
|
|
94
|
+
if (!value || typeof value !== "object") return false;
|
|
95
|
+
const record = value;
|
|
96
|
+
return typeof record.version === "number" && typeof record.acceptedAt === "string" && typeof record.expiresAt === "string" && isValidPreferences(record.preferences);
|
|
97
|
+
}
|
|
98
|
+
function isExpired(record) {
|
|
99
|
+
const expires = Date.parse(record.expiresAt);
|
|
100
|
+
if (Number.isNaN(expires)) return true;
|
|
101
|
+
return expires <= Date.now();
|
|
102
|
+
}
|
|
103
|
+
function createConsentRecord(preferences, config) {
|
|
104
|
+
const { version, expirationDays } = resolveConfig(config);
|
|
105
|
+
const acceptedAt = /* @__PURE__ */ new Date();
|
|
106
|
+
const expiresAt = new Date(
|
|
107
|
+
acceptedAt.getTime() + expirationDays * 24 * 60 * 60 * 1e3
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
version,
|
|
111
|
+
acceptedAt: acceptedAt.toISOString(),
|
|
112
|
+
expiresAt: expiresAt.toISOString(),
|
|
113
|
+
preferences: __spreadProps(__spreadValues({}, preferences), {
|
|
114
|
+
necessary: true
|
|
115
|
+
})
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function readConsent(config) {
|
|
119
|
+
if (!isBrowser()) return null;
|
|
120
|
+
const { version, storageKey } = resolveConfig(config);
|
|
121
|
+
try {
|
|
122
|
+
const raw = localStorage.getItem(storageKey);
|
|
123
|
+
if (!raw) return null;
|
|
124
|
+
const parsed = JSON.parse(raw);
|
|
125
|
+
if (!isValidRecord(parsed)) {
|
|
126
|
+
localStorage.removeItem(storageKey);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (parsed.version !== version || isExpired(parsed)) {
|
|
130
|
+
localStorage.removeItem(storageKey);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return __spreadProps(__spreadValues({}, parsed), {
|
|
134
|
+
preferences: __spreadProps(__spreadValues({}, parsed.preferences), {
|
|
135
|
+
necessary: true
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
} catch (e) {
|
|
139
|
+
try {
|
|
140
|
+
localStorage.removeItem(storageKey);
|
|
141
|
+
} catch (e2) {
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function writeConsent(preferences, config) {
|
|
147
|
+
const record = createConsentRecord(preferences, config);
|
|
148
|
+
if (isBrowser()) {
|
|
149
|
+
const { storageKey } = resolveConfig(config);
|
|
150
|
+
try {
|
|
151
|
+
localStorage.setItem(storageKey, JSON.stringify(record));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return record;
|
|
156
|
+
}
|
|
157
|
+
function clearConsent(config) {
|
|
158
|
+
if (!isBrowser()) return;
|
|
159
|
+
const { storageKey } = resolveConfig(config);
|
|
160
|
+
try {
|
|
161
|
+
localStorage.removeItem(storageKey);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function acceptAllPreferences() {
|
|
166
|
+
return {
|
|
167
|
+
necessary: true,
|
|
168
|
+
preferences: true,
|
|
169
|
+
analytics: true,
|
|
170
|
+
marketing: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function declineAllPreferences() {
|
|
174
|
+
return {
|
|
175
|
+
necessary: true,
|
|
176
|
+
preferences: false,
|
|
177
|
+
analytics: false,
|
|
178
|
+
marketing: false
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// components/crumbie/provider.tsx
|
|
183
|
+
import { jsx } from "react/jsx-runtime";
|
|
184
|
+
var CookieConsentContext = React.createContext(null);
|
|
185
|
+
function CrumbieProvider({
|
|
186
|
+
children,
|
|
187
|
+
config
|
|
188
|
+
}) {
|
|
189
|
+
var _a;
|
|
190
|
+
const [consent, setConsent] = React.useState(null);
|
|
191
|
+
const [isSettingsOpen, setIsSettingsOpen] = React.useState(false);
|
|
192
|
+
const [isHydrated, setIsHydrated] = React.useState(false);
|
|
193
|
+
React.useEffect(() => {
|
|
194
|
+
setConsent(readConsent(config));
|
|
195
|
+
setIsHydrated(true);
|
|
196
|
+
}, []);
|
|
197
|
+
const preferences = (_a = consent == null ? void 0 : consent.preferences) != null ? _a : __spreadProps(__spreadValues(__spreadValues({}, DEFAULT_PREFERENCES), config == null ? void 0 : config.defaultPreferences), {
|
|
198
|
+
necessary: true
|
|
199
|
+
});
|
|
200
|
+
const hasConsent = consent !== null;
|
|
201
|
+
const persist = React.useCallback(
|
|
202
|
+
(next) => {
|
|
203
|
+
const record = writeConsent(
|
|
204
|
+
__spreadProps(__spreadValues({}, next), { necessary: true }),
|
|
205
|
+
config
|
|
206
|
+
);
|
|
207
|
+
setConsent(record);
|
|
208
|
+
},
|
|
209
|
+
[config]
|
|
210
|
+
);
|
|
211
|
+
const acceptAll = React.useCallback(() => {
|
|
212
|
+
persist(acceptAllPreferences());
|
|
213
|
+
setIsSettingsOpen(false);
|
|
214
|
+
}, [persist]);
|
|
215
|
+
const declineAll = React.useCallback(() => {
|
|
216
|
+
persist(declineAllPreferences());
|
|
217
|
+
setIsSettingsOpen(false);
|
|
218
|
+
}, [persist]);
|
|
219
|
+
const updatePreferences = React.useCallback(
|
|
220
|
+
(partial) => {
|
|
221
|
+
const next = __spreadProps(__spreadValues(__spreadValues({}, preferences), partial), {
|
|
222
|
+
necessary: true
|
|
223
|
+
});
|
|
224
|
+
persist(next);
|
|
225
|
+
setIsSettingsOpen(false);
|
|
226
|
+
},
|
|
227
|
+
[persist, preferences]
|
|
228
|
+
);
|
|
229
|
+
const resetConsent = React.useCallback(() => {
|
|
230
|
+
clearConsent(config);
|
|
231
|
+
setConsent(null);
|
|
232
|
+
}, [config]);
|
|
233
|
+
const openSettings = React.useCallback(() => {
|
|
234
|
+
setIsSettingsOpen(true);
|
|
235
|
+
}, []);
|
|
236
|
+
const closeSettings = React.useCallback(() => {
|
|
237
|
+
setIsSettingsOpen(false);
|
|
238
|
+
}, []);
|
|
239
|
+
const setSettingsOpen = React.useCallback((open) => {
|
|
240
|
+
setIsSettingsOpen(open);
|
|
241
|
+
}, []);
|
|
242
|
+
const isAllowed = React.useCallback(
|
|
243
|
+
(category) => {
|
|
244
|
+
if (category === "necessary") return true;
|
|
245
|
+
if (!consent) return false;
|
|
246
|
+
return consent.preferences[category] === true;
|
|
247
|
+
},
|
|
248
|
+
[consent]
|
|
249
|
+
);
|
|
250
|
+
const value = React.useMemo(
|
|
251
|
+
() => ({
|
|
252
|
+
consent,
|
|
253
|
+
preferences,
|
|
254
|
+
hasConsent,
|
|
255
|
+
isSettingsOpen,
|
|
256
|
+
isHydrated,
|
|
257
|
+
acceptAll,
|
|
258
|
+
declineAll,
|
|
259
|
+
updatePreferences,
|
|
260
|
+
resetConsent,
|
|
261
|
+
openSettings,
|
|
262
|
+
closeSettings,
|
|
263
|
+
setSettingsOpen,
|
|
264
|
+
isAllowed
|
|
265
|
+
}),
|
|
266
|
+
[
|
|
267
|
+
consent,
|
|
268
|
+
preferences,
|
|
269
|
+
hasConsent,
|
|
270
|
+
isSettingsOpen,
|
|
271
|
+
isHydrated,
|
|
272
|
+
acceptAll,
|
|
273
|
+
declineAll,
|
|
274
|
+
updatePreferences,
|
|
275
|
+
resetConsent,
|
|
276
|
+
openSettings,
|
|
277
|
+
closeSettings,
|
|
278
|
+
setSettingsOpen,
|
|
279
|
+
isAllowed
|
|
280
|
+
]
|
|
281
|
+
);
|
|
282
|
+
return /* @__PURE__ */ jsx(CookieConsentContext.Provider, { value, children });
|
|
283
|
+
}
|
|
284
|
+
function useCookieConsentContext() {
|
|
285
|
+
const context = React.useContext(CookieConsentContext);
|
|
286
|
+
if (!context) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
"useCookieConsent must be used within a CrumbieProvider"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return context;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// components/crumbie/cookie-banner.tsx
|
|
295
|
+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
296
|
+
import Image from "next/image";
|
|
297
|
+
|
|
298
|
+
// components/ui/button.tsx
|
|
299
|
+
import * as React2 from "react";
|
|
300
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
301
|
+
import { cva } from "class-variance-authority";
|
|
302
|
+
|
|
303
|
+
// lib/utils.ts
|
|
304
|
+
import { clsx } from "clsx";
|
|
305
|
+
import { twMerge } from "tailwind-merge";
|
|
306
|
+
function cn(...inputs) {
|
|
307
|
+
return twMerge(clsx(inputs));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// components/ui/button.tsx
|
|
311
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
312
|
+
var buttonVariants = cva(
|
|
313
|
+
"inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
|
|
314
|
+
{
|
|
315
|
+
variants: {
|
|
316
|
+
variant: {
|
|
317
|
+
default: "bg-foreground text-background hover:bg-foreground/90",
|
|
318
|
+
secondary: "bg-muted text-foreground hover:bg-muted/80",
|
|
319
|
+
outline: "border border-border bg-transparent text-foreground hover:bg-muted/60",
|
|
320
|
+
ghost: "hover:bg-muted/60 text-foreground",
|
|
321
|
+
link: "text-foreground underline-offset-4 hover:underline"
|
|
322
|
+
},
|
|
323
|
+
size: {
|
|
324
|
+
default: "h-10 px-5",
|
|
325
|
+
sm: "h-9 px-4 text-sm",
|
|
326
|
+
lg: "h-11 px-6 text-base",
|
|
327
|
+
icon: "h-10 w-10"
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
defaultVariants: {
|
|
331
|
+
variant: "default",
|
|
332
|
+
size: "default"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
var Button = React2.forwardRef(
|
|
337
|
+
(_a, ref) => {
|
|
338
|
+
var _b = _a, { className, variant, size, asChild = false } = _b, props = __objRest(_b, ["className", "variant", "size", "asChild"]);
|
|
339
|
+
const Comp = asChild ? Slot : "button";
|
|
340
|
+
return /* @__PURE__ */ jsx2(
|
|
341
|
+
Comp,
|
|
342
|
+
__spreadValues({
|
|
343
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
344
|
+
ref
|
|
345
|
+
}, props)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
Button.displayName = "Button";
|
|
350
|
+
|
|
351
|
+
// components/crumbie/assets/cookie.png
|
|
352
|
+
var cookie_default = "./cookie-QMHHGW4Q.png";
|
|
353
|
+
|
|
354
|
+
// hooks/use-cookie-consent.ts
|
|
355
|
+
function useCookieConsent() {
|
|
356
|
+
return useCookieConsentContext();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// components/crumbie/cookie-banner.tsx
|
|
360
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
361
|
+
function CookieBanner({
|
|
362
|
+
title = "We value your privacy",
|
|
363
|
+
description = "We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking Accept All, you consent to our use of cookies.",
|
|
364
|
+
className,
|
|
365
|
+
position = "left"
|
|
366
|
+
}) {
|
|
367
|
+
const { hasConsent, isHydrated, acceptAll, declineAll, openSettings } = useCookieConsent();
|
|
368
|
+
const prefersReducedMotion = useReducedMotion();
|
|
369
|
+
const show = isHydrated && !hasConsent;
|
|
370
|
+
const positionClass = position === "left" || position === "bottom-left" ? "left-3 right-3 sm:left-4 sm:right-auto sm:max-w-md" : position === "right" || position === "bottom-right" ? "left-3 right-3 sm:right-4 sm:left-auto sm:max-w-md" : "left-3 right-3 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 sm:max-w-lg";
|
|
371
|
+
return /* @__PURE__ */ jsx3(AnimatePresence, { children: show ? /* @__PURE__ */ jsx3(
|
|
372
|
+
motion.div,
|
|
373
|
+
{
|
|
374
|
+
role: "dialog",
|
|
375
|
+
"aria-modal": "false",
|
|
376
|
+
"aria-labelledby": "crumbie-banner-title",
|
|
377
|
+
"aria-describedby": "crumbie-banner-description",
|
|
378
|
+
initial: prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 16 },
|
|
379
|
+
animate: prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 },
|
|
380
|
+
exit: prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 12 },
|
|
381
|
+
transition: { duration: 0.24, ease: [0.22, 1, 0.36, 1] },
|
|
382
|
+
className: cn(
|
|
383
|
+
"fixed bottom-3 z-50 sm:bottom-4",
|
|
384
|
+
positionClass,
|
|
385
|
+
className
|
|
386
|
+
),
|
|
387
|
+
children: /* @__PURE__ */ jsxs("div", { className: "rounded-2xl border border-border bg-background p-5 shadow-[0_6px_24px_rgb(0,0,0,0.06)] sm:p-6", children: [
|
|
388
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-3", children: [
|
|
389
|
+
/* @__PURE__ */ jsx3(
|
|
390
|
+
"div",
|
|
391
|
+
{
|
|
392
|
+
className: "flex h-10 w-10 shrink-0 items-center justify-center",
|
|
393
|
+
"aria-hidden": true,
|
|
394
|
+
children: /* @__PURE__ */ jsx3(
|
|
395
|
+
Image,
|
|
396
|
+
{
|
|
397
|
+
src: cookie_default,
|
|
398
|
+
alt: "",
|
|
399
|
+
width: 36,
|
|
400
|
+
height: 36,
|
|
401
|
+
className: "h-9 w-9 object-contain"
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
),
|
|
406
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
407
|
+
/* @__PURE__ */ jsx3(
|
|
408
|
+
"h2",
|
|
409
|
+
{
|
|
410
|
+
id: "crumbie-banner-title",
|
|
411
|
+
className: "text-base font-semibold tracking-tight text-foreground",
|
|
412
|
+
children: title
|
|
413
|
+
}
|
|
414
|
+
),
|
|
415
|
+
/* @__PURE__ */ jsx3(
|
|
416
|
+
"p",
|
|
417
|
+
{
|
|
418
|
+
id: "crumbie-banner-description",
|
|
419
|
+
className: "text-sm leading-relaxed text-muted-foreground",
|
|
420
|
+
children: description
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
] })
|
|
424
|
+
] }),
|
|
425
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 flex flex-wrap items-center gap-2", children: [
|
|
426
|
+
/* @__PURE__ */ jsx3(Button, { variant: "outline", size: "sm", onClick: openSettings, children: "Cookie Settings" }),
|
|
427
|
+
/* @__PURE__ */ jsx3(Button, { variant: "secondary", size: "sm", onClick: declineAll, children: "Decline" }),
|
|
428
|
+
/* @__PURE__ */ jsx3(Button, { size: "sm", onClick: acceptAll, children: "Accept All" })
|
|
429
|
+
] })
|
|
430
|
+
] })
|
|
431
|
+
}
|
|
432
|
+
) : null });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// components/crumbie/cookie-settings.tsx
|
|
436
|
+
import * as React5 from "react";
|
|
437
|
+
|
|
438
|
+
// components/ui/dialog.tsx
|
|
439
|
+
import * as React3 from "react";
|
|
440
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
441
|
+
import { X } from "lucide-react";
|
|
442
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
443
|
+
var Dialog = DialogPrimitive.Root;
|
|
444
|
+
var DialogPortal = DialogPrimitive.Portal;
|
|
445
|
+
var DialogOverlay = React3.forwardRef((_a, ref) => {
|
|
446
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
447
|
+
return /* @__PURE__ */ jsx4(
|
|
448
|
+
DialogPrimitive.Overlay,
|
|
449
|
+
__spreadValues({
|
|
450
|
+
ref,
|
|
451
|
+
className: cn("fixed inset-0 z-50 bg-black/40", className)
|
|
452
|
+
}, props)
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
456
|
+
var DialogContent = React3.forwardRef((_a, ref) => {
|
|
457
|
+
var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
|
|
458
|
+
return /* @__PURE__ */ jsxs2(DialogPortal, { children: [
|
|
459
|
+
/* @__PURE__ */ jsx4(DialogOverlay, {}),
|
|
460
|
+
/* @__PURE__ */ jsxs2(
|
|
461
|
+
DialogPrimitive.Content,
|
|
462
|
+
__spreadProps(__spreadValues({
|
|
463
|
+
ref,
|
|
464
|
+
className: cn(
|
|
465
|
+
"fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-1.5rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-0 rounded-lg border border-border bg-background p-0 shadow-lg outline-none",
|
|
466
|
+
className
|
|
467
|
+
)
|
|
468
|
+
}, props), {
|
|
469
|
+
children: [
|
|
470
|
+
children,
|
|
471
|
+
/* @__PURE__ */ jsxs2(DialogPrimitive.Close, { className: "absolute right-3 top-3 rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [
|
|
472
|
+
/* @__PURE__ */ jsx4(X, { className: "h-3.5 w-3.5" }),
|
|
473
|
+
/* @__PURE__ */ jsx4("span", { className: "sr-only", children: "Close" })
|
|
474
|
+
] })
|
|
475
|
+
]
|
|
476
|
+
})
|
|
477
|
+
)
|
|
478
|
+
] });
|
|
479
|
+
});
|
|
480
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
481
|
+
function DialogHeader(_a) {
|
|
482
|
+
var _b = _a, {
|
|
483
|
+
className
|
|
484
|
+
} = _b, props = __objRest(_b, [
|
|
485
|
+
"className"
|
|
486
|
+
]);
|
|
487
|
+
return /* @__PURE__ */ jsx4(
|
|
488
|
+
"div",
|
|
489
|
+
__spreadValues({
|
|
490
|
+
className: cn("flex flex-col gap-1 px-4 pt-4 pr-10", className)
|
|
491
|
+
}, props)
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
function DialogFooter(_a) {
|
|
495
|
+
var _b = _a, {
|
|
496
|
+
className
|
|
497
|
+
} = _b, props = __objRest(_b, [
|
|
498
|
+
"className"
|
|
499
|
+
]);
|
|
500
|
+
return /* @__PURE__ */ jsx4(
|
|
501
|
+
"div",
|
|
502
|
+
__spreadValues({
|
|
503
|
+
className: cn(
|
|
504
|
+
"flex flex-col-reverse gap-1.5 border-t border-border px-4 py-3 sm:flex-row sm:justify-end",
|
|
505
|
+
className
|
|
506
|
+
)
|
|
507
|
+
}, props)
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
var DialogTitle = React3.forwardRef((_a, ref) => {
|
|
511
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
512
|
+
return /* @__PURE__ */ jsx4(
|
|
513
|
+
DialogPrimitive.Title,
|
|
514
|
+
__spreadValues({
|
|
515
|
+
ref,
|
|
516
|
+
className: cn(
|
|
517
|
+
"text-sm font-semibold tracking-tight text-foreground",
|
|
518
|
+
className
|
|
519
|
+
)
|
|
520
|
+
}, props)
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
524
|
+
var DialogDescription = React3.forwardRef((_a, ref) => {
|
|
525
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
526
|
+
return /* @__PURE__ */ jsx4(
|
|
527
|
+
DialogPrimitive.Description,
|
|
528
|
+
__spreadValues({
|
|
529
|
+
ref,
|
|
530
|
+
className: cn("text-xs leading-relaxed text-muted-foreground", className)
|
|
531
|
+
}, props)
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
535
|
+
|
|
536
|
+
// components/ui/switch.tsx
|
|
537
|
+
import * as React4 from "react";
|
|
538
|
+
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
|
539
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
540
|
+
var Switch = React4.forwardRef((_a, ref) => {
|
|
541
|
+
var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
|
|
542
|
+
return /* @__PURE__ */ jsx5(
|
|
543
|
+
SwitchPrimitives.Root,
|
|
544
|
+
__spreadProps(__spreadValues({
|
|
545
|
+
className: cn(
|
|
546
|
+
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-muted",
|
|
547
|
+
className
|
|
548
|
+
)
|
|
549
|
+
}, props), {
|
|
550
|
+
ref,
|
|
551
|
+
children: /* @__PURE__ */ jsx5(
|
|
552
|
+
SwitchPrimitives.Thumb,
|
|
553
|
+
{
|
|
554
|
+
className: cn(
|
|
555
|
+
"pointer-events-none block h-3.5 w-3.5 rounded-full bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5"
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
})
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
Switch.displayName = SwitchPrimitives.Root.displayName;
|
|
563
|
+
|
|
564
|
+
// components/crumbie/cookie-settings.tsx
|
|
565
|
+
import styles from "./cookie-settings.module-DU4XK62C.module.css";
|
|
566
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
567
|
+
function CookieSettings({
|
|
568
|
+
title = "Cookie Settings",
|
|
569
|
+
description = "Manage your cookie preferences. Necessary cookies are always enabled to keep the site working.",
|
|
570
|
+
className
|
|
571
|
+
}) {
|
|
572
|
+
const {
|
|
573
|
+
isSettingsOpen,
|
|
574
|
+
setSettingsOpen,
|
|
575
|
+
preferences,
|
|
576
|
+
acceptAll,
|
|
577
|
+
declineAll,
|
|
578
|
+
updatePreferences
|
|
579
|
+
} = useCookieConsent();
|
|
580
|
+
const [draft, setDraft] = React5.useState(preferences);
|
|
581
|
+
React5.useEffect(() => {
|
|
582
|
+
if (isSettingsOpen) {
|
|
583
|
+
setDraft(preferences);
|
|
584
|
+
}
|
|
585
|
+
}, [isSettingsOpen, preferences]);
|
|
586
|
+
const handleToggle = (key, value) => {
|
|
587
|
+
if (key === "necessary") return;
|
|
588
|
+
setDraft((prev) => __spreadProps(__spreadValues({}, prev), { [key]: value, necessary: true }));
|
|
589
|
+
};
|
|
590
|
+
const handleSave = () => {
|
|
591
|
+
updatePreferences(draft);
|
|
592
|
+
};
|
|
593
|
+
return /* @__PURE__ */ jsx6(Dialog, { open: isSettingsOpen, onOpenChange: setSettingsOpen, children: /* @__PURE__ */ jsxs3(
|
|
594
|
+
DialogContent,
|
|
595
|
+
{
|
|
596
|
+
className: cn("max-h-[min(90vh,520px)] overflow-hidden", className),
|
|
597
|
+
children: [
|
|
598
|
+
/* @__PURE__ */ jsxs3(DialogHeader, { children: [
|
|
599
|
+
/* @__PURE__ */ jsx6(DialogTitle, { children: title }),
|
|
600
|
+
/* @__PURE__ */ jsx6(DialogDescription, { children: description })
|
|
601
|
+
] }),
|
|
602
|
+
/* @__PURE__ */ jsx6(
|
|
603
|
+
"div",
|
|
604
|
+
{
|
|
605
|
+
className: cn(
|
|
606
|
+
"max-h-[min(48vh,300px)] space-y-0.5 overflow-y-auto px-4 py-2.5",
|
|
607
|
+
styles.scrollArea
|
|
608
|
+
),
|
|
609
|
+
children: COOKIE_CATEGORIES.map((category) => {
|
|
610
|
+
const checked = draft[category.id];
|
|
611
|
+
const disabled = category.required;
|
|
612
|
+
return /* @__PURE__ */ jsxs3(
|
|
613
|
+
"div",
|
|
614
|
+
{
|
|
615
|
+
className: "flex items-start justify-between gap-3 py-2.5",
|
|
616
|
+
children: [
|
|
617
|
+
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 space-y-0.5 pr-1.5", children: [
|
|
618
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1.5", children: [
|
|
619
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs font-medium text-foreground", children: category.title }),
|
|
620
|
+
category.required ? /* @__PURE__ */ jsx6("span", { className: "rounded-lg bg-muted px-1.5 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground", children: "Always on" }) : null
|
|
621
|
+
] }),
|
|
622
|
+
/* @__PURE__ */ jsx6("p", { className: "text-[11px] leading-relaxed text-muted-foreground", children: category.description })
|
|
623
|
+
] }),
|
|
624
|
+
/* @__PURE__ */ jsx6(
|
|
625
|
+
Switch,
|
|
626
|
+
{
|
|
627
|
+
checked,
|
|
628
|
+
disabled,
|
|
629
|
+
onCheckedChange: (value) => handleToggle(category.id, value),
|
|
630
|
+
"aria-label": `${category.title} cookies`,
|
|
631
|
+
className: "mt-0.5"
|
|
632
|
+
}
|
|
633
|
+
)
|
|
634
|
+
]
|
|
635
|
+
},
|
|
636
|
+
category.id
|
|
637
|
+
);
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
),
|
|
641
|
+
/* @__PURE__ */ jsxs3(DialogFooter, { children: [
|
|
642
|
+
/* @__PURE__ */ jsx6(
|
|
643
|
+
Button,
|
|
644
|
+
{
|
|
645
|
+
variant: "outline",
|
|
646
|
+
size: "sm",
|
|
647
|
+
className: "w-full sm:w-auto",
|
|
648
|
+
onClick: declineAll,
|
|
649
|
+
children: "Decline Optional"
|
|
650
|
+
}
|
|
651
|
+
),
|
|
652
|
+
/* @__PURE__ */ jsx6(
|
|
653
|
+
Button,
|
|
654
|
+
{
|
|
655
|
+
variant: "secondary",
|
|
656
|
+
size: "sm",
|
|
657
|
+
className: "w-full sm:w-auto",
|
|
658
|
+
onClick: handleSave,
|
|
659
|
+
children: "Save Preferences"
|
|
660
|
+
}
|
|
661
|
+
),
|
|
662
|
+
/* @__PURE__ */ jsx6(Button, { size: "sm", className: "w-full sm:w-auto", onClick: acceptAll, children: "Accept All" })
|
|
663
|
+
] })
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
) });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// components/crumbie/cookie-settings-button.tsx
|
|
670
|
+
import { Settings2 } from "lucide-react";
|
|
671
|
+
import Image2 from "next/image";
|
|
672
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
673
|
+
function CookieSettingsButton({
|
|
674
|
+
className,
|
|
675
|
+
label = "Cookie settings",
|
|
676
|
+
variant = "floating"
|
|
677
|
+
}) {
|
|
678
|
+
const { hasConsent, isHydrated, openSettings } = useCookieConsent();
|
|
679
|
+
if (!isHydrated || !hasConsent) return null;
|
|
680
|
+
if (variant === "inline") {
|
|
681
|
+
return /* @__PURE__ */ jsxs4(
|
|
682
|
+
Button,
|
|
683
|
+
{
|
|
684
|
+
variant: "outline",
|
|
685
|
+
size: "sm",
|
|
686
|
+
onClick: openSettings,
|
|
687
|
+
className,
|
|
688
|
+
"aria-label": label,
|
|
689
|
+
children: [
|
|
690
|
+
/* @__PURE__ */ jsx7(Settings2, { className: "h-3.5 w-3.5" }),
|
|
691
|
+
label
|
|
692
|
+
]
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return /* @__PURE__ */ jsx7(
|
|
697
|
+
Button,
|
|
698
|
+
{
|
|
699
|
+
variant: "outline",
|
|
700
|
+
size: "icon",
|
|
701
|
+
onClick: openSettings,
|
|
702
|
+
"aria-label": label,
|
|
703
|
+
className: cn(
|
|
704
|
+
"fixed bottom-3 left-3 z-40 h-9 w-9 rounded-full border-border bg-background shadow-[0_3px_14px_rgb(0,0,0,0.08)] hover:bg-muted sm:bottom-4 sm:left-4",
|
|
705
|
+
className
|
|
706
|
+
),
|
|
707
|
+
children: /* @__PURE__ */ jsx7(
|
|
708
|
+
Image2,
|
|
709
|
+
{
|
|
710
|
+
src: cookie_default,
|
|
711
|
+
alt: "",
|
|
712
|
+
width: 28,
|
|
713
|
+
height: 28,
|
|
714
|
+
className: "h-7 w-7 object-contain"
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// hooks/use-consent-category.ts
|
|
722
|
+
function useConsentCategory(category) {
|
|
723
|
+
const { isAllowed, isHydrated } = useCookieConsentContext();
|
|
724
|
+
if (!isHydrated) return category === "necessary";
|
|
725
|
+
return isAllowed(category);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// components/crumbie/consent-gate.tsx
|
|
729
|
+
import { Fragment, jsx as jsx8 } from "react/jsx-runtime";
|
|
730
|
+
function ConsentGate({
|
|
731
|
+
category,
|
|
732
|
+
children,
|
|
733
|
+
fallback = null
|
|
734
|
+
}) {
|
|
735
|
+
const allowed = useConsentCategory(category);
|
|
736
|
+
if (!allowed) return /* @__PURE__ */ jsx8(Fragment, { children: fallback });
|
|
737
|
+
return /* @__PURE__ */ jsx8(Fragment, { children });
|
|
738
|
+
}
|
|
739
|
+
export {
|
|
740
|
+
COOKIE_CATEGORIES,
|
|
741
|
+
ConsentGate,
|
|
742
|
+
CookieBanner,
|
|
743
|
+
CookieSettings,
|
|
744
|
+
CookieSettingsButton,
|
|
745
|
+
CrumbieProvider,
|
|
746
|
+
DEFAULT_PREFERENCES,
|
|
747
|
+
useConsentCategory,
|
|
748
|
+
useCookieConsent
|
|
749
|
+
};
|
|
750
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../components/crumbie/provider.tsx","../types/consent.ts","../lib/storage.ts","../components/crumbie/cookie-banner.tsx","../components/ui/button.tsx","../lib/utils.ts","../hooks/use-cookie-consent.ts","../components/crumbie/cookie-settings.tsx","../components/ui/dialog.tsx","../components/ui/switch.tsx","../components/crumbie/cookie-settings-button.tsx","../hooks/use-consent-category.ts","../components/crumbie/consent-gate.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport {\n acceptAllPreferences,\n clearConsent,\n declineAllPreferences,\n readConsent,\n writeConsent,\n} from \"@/lib/storage\";\nimport {\n DEFAULT_PREFERENCES,\n type ConsentConfig,\n type ConsentPreferences,\n type ConsentRecord,\n type CookieCategory,\n} from \"@/types/consent\";\n\nexport type CookieConsentContextValue = {\n consent: ConsentRecord | null;\n preferences: ConsentPreferences;\n hasConsent: boolean;\n isSettingsOpen: boolean;\n isHydrated: boolean;\n acceptAll: () => void;\n declineAll: () => void;\n updatePreferences: (preferences: Partial<ConsentPreferences>) => void;\n resetConsent: () => void;\n openSettings: () => void;\n closeSettings: () => void;\n setSettingsOpen: (open: boolean) => void;\n isAllowed: (category: CookieCategory) => boolean;\n};\n\nconst CookieConsentContext =\n React.createContext<CookieConsentContextValue | null>(null);\n\nexport type CrumbieProviderProps = {\n children: React.ReactNode;\n config?: ConsentConfig;\n};\n\nexport function CrumbieProvider({\n children,\n config,\n}: CrumbieProviderProps) {\n const [consent, setConsent] = React.useState<ConsentRecord | null>(null);\n const [isSettingsOpen, setIsSettingsOpen] = React.useState(false);\n const [isHydrated, setIsHydrated] = React.useState(false);\n\n React.useEffect(() => {\n setConsent(readConsent(config));\n setIsHydrated(true);\n // config is expected to be stable; re-read only on mount\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const preferences = consent?.preferences ?? {\n ...DEFAULT_PREFERENCES,\n ...config?.defaultPreferences,\n necessary: true as const,\n };\n\n const hasConsent = consent !== null;\n\n const persist = React.useCallback(\n (next: ConsentPreferences) => {\n const record = writeConsent(\n { ...next, necessary: true },\n config,\n );\n setConsent(record);\n },\n [config],\n );\n\n const acceptAll = React.useCallback(() => {\n persist(acceptAllPreferences());\n setIsSettingsOpen(false);\n }, [persist]);\n\n const declineAll = React.useCallback(() => {\n persist(declineAllPreferences());\n setIsSettingsOpen(false);\n }, [persist]);\n\n const updatePreferences = React.useCallback(\n (partial: Partial<ConsentPreferences>) => {\n const next: ConsentPreferences = {\n ...preferences,\n ...partial,\n necessary: true,\n };\n persist(next);\n setIsSettingsOpen(false);\n },\n [persist, preferences],\n );\n\n const resetConsent = React.useCallback(() => {\n clearConsent(config);\n setConsent(null);\n }, [config]);\n\n const openSettings = React.useCallback(() => {\n setIsSettingsOpen(true);\n }, []);\n\n const closeSettings = React.useCallback(() => {\n setIsSettingsOpen(false);\n }, []);\n\n const setSettingsOpen = React.useCallback((open: boolean) => {\n setIsSettingsOpen(open);\n }, []);\n\n const isAllowed = React.useCallback(\n (category: CookieCategory) => {\n if (category === \"necessary\") return true;\n if (!consent) return false;\n return consent.preferences[category] === true;\n },\n [consent],\n );\n\n const value = React.useMemo<CookieConsentContextValue>(\n () => ({\n consent,\n preferences,\n hasConsent,\n isSettingsOpen,\n isHydrated,\n acceptAll,\n declineAll,\n updatePreferences,\n resetConsent,\n openSettings,\n closeSettings,\n setSettingsOpen,\n isAllowed,\n }),\n [\n consent,\n preferences,\n hasConsent,\n isSettingsOpen,\n isHydrated,\n acceptAll,\n declineAll,\n updatePreferences,\n resetConsent,\n openSettings,\n closeSettings,\n setSettingsOpen,\n isAllowed,\n ],\n );\n\n return (\n <CookieConsentContext.Provider value={value}>\n {children}\n </CookieConsentContext.Provider>\n );\n}\n\nexport function useCookieConsentContext(): CookieConsentContextValue {\n const context = React.useContext(CookieConsentContext);\n if (!context) {\n throw new Error(\n \"useCookieConsent must be used within a CrumbieProvider\",\n );\n }\n return context;\n}\n","export type CookieCategory =\n | \"necessary\"\n | \"preferences\"\n | \"analytics\"\n | \"marketing\";\n\nexport type ConsentPreferences = {\n necessary: true;\n preferences: boolean;\n analytics: boolean;\n marketing: boolean;\n};\n\nexport type ConsentRecord = {\n version: number;\n acceptedAt: string;\n expiresAt: string;\n preferences: ConsentPreferences;\n};\n\nexport type ConsentConfig = {\n version?: number;\n storageKey?: string;\n expirationDays?: number;\n defaultPreferences?: Partial<\n Omit<ConsentPreferences, \"necessary\">\n >;\n};\n\nexport type CookieCategoryMeta = {\n id: CookieCategory;\n title: string;\n description: string;\n required: boolean;\n};\n\nexport const COOKIE_CATEGORIES: CookieCategoryMeta[] = [\n {\n id: \"necessary\",\n title: \"Necessary\",\n description:\n \"Essential cookies required for the website to function. These cannot be disabled.\",\n required: true,\n },\n {\n id: \"preferences\",\n title: \"Preferences\",\n description:\n \"Cookies that remember your settings and preferences for a more personalized experience.\",\n required: false,\n },\n {\n id: \"analytics\",\n title: \"Analytics\",\n description:\n \"Cookies that help us understand how visitors interact with our website.\",\n required: false,\n },\n {\n id: \"marketing\",\n title: \"Marketing\",\n description:\n \"Cookies used to deliver relevant ads and measure campaign performance.\",\n required: false,\n },\n];\n\nexport const DEFAULT_PREFERENCES: ConsentPreferences = {\n necessary: true,\n preferences: false,\n analytics: false,\n marketing: false,\n};\n\nexport const CONSENT_VERSION = 1;\nexport const STORAGE_KEY = \"crumbie-consent\";\nexport const EXPIRATION_DAYS = 365;\n","import {\n CONSENT_VERSION,\n DEFAULT_PREFERENCES,\n EXPIRATION_DAYS,\n STORAGE_KEY,\n type ConsentConfig,\n type ConsentPreferences,\n type ConsentRecord,\n} from \"@/types/consent\";\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof localStorage !== \"undefined\";\n}\n\nfunction resolveConfig(config?: ConsentConfig) {\n return {\n version: config?.version ?? CONSENT_VERSION,\n storageKey: config?.storageKey ?? STORAGE_KEY,\n expirationDays: config?.expirationDays ?? EXPIRATION_DAYS,\n defaultPreferences: {\n ...DEFAULT_PREFERENCES,\n ...config?.defaultPreferences,\n necessary: true as const,\n },\n };\n}\n\nfunction isValidPreferences(\n value: unknown,\n): value is ConsentPreferences {\n if (!value || typeof value !== \"object\") return false;\n const prefs = value as Record<string, unknown>;\n return (\n prefs.necessary === true &&\n typeof prefs.preferences === \"boolean\" &&\n typeof prefs.analytics === \"boolean\" &&\n typeof prefs.marketing === \"boolean\"\n );\n}\n\nfunction isValidRecord(value: unknown): value is ConsentRecord {\n if (!value || typeof value !== \"object\") return false;\n const record = value as Record<string, unknown>;\n return (\n typeof record.version === \"number\" &&\n typeof record.acceptedAt === \"string\" &&\n typeof record.expiresAt === \"string\" &&\n isValidPreferences(record.preferences)\n );\n}\n\nfunction isExpired(record: ConsentRecord): boolean {\n const expires = Date.parse(record.expiresAt);\n if (Number.isNaN(expires)) return true;\n return expires <= Date.now();\n}\n\nexport function createConsentRecord(\n preferences: ConsentPreferences,\n config?: ConsentConfig,\n): ConsentRecord {\n const { version, expirationDays } = resolveConfig(config);\n const acceptedAt = new Date();\n const expiresAt = new Date(\n acceptedAt.getTime() + expirationDays * 24 * 60 * 60 * 1000,\n );\n\n return {\n version,\n acceptedAt: acceptedAt.toISOString(),\n expiresAt: expiresAt.toISOString(),\n preferences: {\n ...preferences,\n necessary: true,\n },\n };\n}\n\nexport function readConsent(\n config?: ConsentConfig,\n): ConsentRecord | null {\n if (!isBrowser()) return null;\n\n const { version, storageKey } = resolveConfig(config);\n\n try {\n const raw = localStorage.getItem(storageKey);\n if (!raw) return null;\n\n const parsed: unknown = JSON.parse(raw);\n if (!isValidRecord(parsed)) {\n localStorage.removeItem(storageKey);\n return null;\n }\n\n if (parsed.version !== version || isExpired(parsed)) {\n localStorage.removeItem(storageKey);\n return null;\n }\n\n return {\n ...parsed,\n preferences: {\n ...parsed.preferences,\n necessary: true,\n },\n };\n } catch {\n try {\n localStorage.removeItem(storageKey);\n } catch {\n // ignore cleanup failures\n }\n return null;\n }\n}\n\nexport function writeConsent(\n preferences: ConsentPreferences,\n config?: ConsentConfig,\n): ConsentRecord {\n const record = createConsentRecord(preferences, config);\n\n if (isBrowser()) {\n const { storageKey } = resolveConfig(config);\n try {\n localStorage.setItem(storageKey, JSON.stringify(record));\n } catch {\n // storage may be unavailable (private mode quotas)\n }\n }\n\n return record;\n}\n\nexport function clearConsent(config?: ConsentConfig): void {\n if (!isBrowser()) return;\n const { storageKey } = resolveConfig(config);\n try {\n localStorage.removeItem(storageKey);\n } catch {\n // ignore\n }\n}\n\nexport function acceptAllPreferences(): ConsentPreferences {\n return {\n necessary: true,\n preferences: true,\n analytics: true,\n marketing: true,\n };\n}\n\nexport function declineAllPreferences(): ConsentPreferences {\n return {\n necessary: true,\n preferences: false,\n analytics: false,\n marketing: false,\n };\n}\n","\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport Image from \"next/image\";\nimport { Button } from \"@/components/ui/button\";\nimport cookieImage from \"@/components/crumbie/assets/cookie.png\";\nimport { useCookieConsent } from \"@/hooks/use-cookie-consent\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CookieBannerProps = {\n title?: string;\n description?: string;\n className?: string;\n position?:\n \"left\" | \"center\" | \"right\" | \"bottom\" | \"bottom-left\" | \"bottom-right\";\n};\n\nexport function CookieBanner({\n title = \"We value your privacy\",\n description = \"We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking Accept All, you consent to our use of cookies.\",\n className,\n position = \"left\",\n}: CookieBannerProps) {\n const { hasConsent, isHydrated, acceptAll, declineAll, openSettings } =\n useCookieConsent();\n const prefersReducedMotion = useReducedMotion();\n\n const show = isHydrated && !hasConsent;\n\n const positionClass =\n position === \"left\" || position === \"bottom-left\"\n ? \"left-3 right-3 sm:left-4 sm:right-auto sm:max-w-md\"\n : position === \"right\" || position === \"bottom-right\"\n ? \"left-3 right-3 sm:right-4 sm:left-auto sm:max-w-md\"\n : \"left-3 right-3 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 sm:max-w-lg\";\n\n return (\n <AnimatePresence>\n {show ? (\n <motion.div\n role=\"dialog\"\n aria-modal=\"false\"\n aria-labelledby=\"crumbie-banner-title\"\n aria-describedby=\"crumbie-banner-description\"\n initial={\n prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 16 }\n }\n animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 12 }}\n transition={{ duration: 0.24, ease: [0.22, 1, 0.36, 1] }}\n className={cn(\n \"fixed bottom-3 z-50 sm:bottom-4\",\n positionClass,\n className,\n )}\n >\n <div className=\"rounded-2xl border border-border bg-background p-5 shadow-[0_6px_24px_rgb(0,0,0,0.06)] sm:p-6\">\n <div className=\"flex flex-col items-start gap-3\">\n <div\n className=\"flex h-10 w-10 shrink-0 items-center justify-center\"\n aria-hidden\n >\n <Image\n src={cookieImage}\n alt=\"\"\n width={36}\n height={36}\n className=\"h-9 w-9 object-contain\"\n />\n </div>\n <div className=\"space-y-0.5\">\n <h2\n id=\"crumbie-banner-title\"\n className=\"text-base font-semibold tracking-tight text-foreground\"\n >\n {title}\n </h2>\n <p\n id=\"crumbie-banner-description\"\n className=\"text-sm leading-relaxed text-muted-foreground\"\n >\n {description}\n </p>\n </div>\n </div>\n\n <div className=\"mt-5 flex flex-wrap items-center gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={openSettings}>\n Cookie Settings\n </Button>\n <Button variant=\"secondary\" size=\"sm\" onClick={declineAll}>\n Decline\n </Button>\n <Button size=\"sm\" onClick={acceptAll}>\n Accept All\n </Button>\n </div>\n </div>\n </motion.div>\n ) : null}\n </AnimatePresence>\n );\n}\n","import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 cursor-pointer\",\n {\n variants: {\n variant: {\n default: \"bg-foreground text-background hover:bg-foreground/90\",\n secondary: \"bg-muted text-foreground hover:bg-muted/80\",\n outline:\n \"border border-border bg-transparent text-foreground hover:bg-muted/60\",\n ghost: \"hover:bg-muted/60 text-foreground\",\n link: \"text-foreground underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-5\",\n sm: \"h-9 px-4 text-sm\",\n lg: \"h-11 px-6 text-base\",\n icon: \"h-10 w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport interface ButtonProps\n extends\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n className={cn(buttonVariants({ variant, size, className }))}\n ref={ref}\n {...props}\n />\n );\n },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport { useCookieConsentContext } from \"@/components/crumbie/provider\";\n\nexport function useCookieConsent() {\n return useCookieConsentContext();\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useCookieConsent } from \"@/hooks/use-cookie-consent\";\nimport { COOKIE_CATEGORIES, type ConsentPreferences } from \"@/types/consent\";\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./cookie-settings.module.css\";\n\nexport type CookieSettingsProps = {\n title?: string;\n description?: string;\n className?: string;\n};\n\nexport function CookieSettings({\n title = \"Cookie Settings\",\n description = \"Manage your cookie preferences. Necessary cookies are always enabled to keep the site working.\",\n className,\n}: CookieSettingsProps) {\n const {\n isSettingsOpen,\n setSettingsOpen,\n preferences,\n acceptAll,\n declineAll,\n updatePreferences,\n } = useCookieConsent();\n\n const [draft, setDraft] = React.useState<ConsentPreferences>(preferences);\n\n React.useEffect(() => {\n if (isSettingsOpen) {\n setDraft(preferences);\n }\n }, [isSettingsOpen, preferences]);\n\n const handleToggle = (key: keyof ConsentPreferences, value: boolean) => {\n if (key === \"necessary\") return;\n setDraft((prev) => ({ ...prev, [key]: value, necessary: true }));\n };\n\n const handleSave = () => {\n updatePreferences(draft);\n };\n\n return (\n <Dialog open={isSettingsOpen} onOpenChange={setSettingsOpen}>\n <DialogContent\n className={cn(\"max-h-[min(90vh,520px)] overflow-hidden\", className)}\n >\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n\n <div\n className={cn(\n \"max-h-[min(48vh,300px)] space-y-0.5 overflow-y-auto px-4 py-2.5\",\n styles.scrollArea,\n )}\n >\n {COOKIE_CATEGORIES.map((category) => {\n const checked = draft[category.id];\n const disabled = category.required;\n\n return (\n <div\n key={category.id}\n className=\"flex items-start justify-between gap-3 py-2.5\"\n >\n <div className=\"min-w-0 space-y-0.5 pr-1.5\">\n <div className=\"flex items-center gap-1.5\">\n <p className=\"text-xs font-medium text-foreground\">\n {category.title}\n </p>\n {category.required ? (\n <span className=\"rounded-lg bg-muted px-1.5 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground\">\n Always on\n </span>\n ) : null}\n </div>\n <p className=\"text-[11px] leading-relaxed text-muted-foreground\">\n {category.description}\n </p>\n </div>\n <Switch\n checked={checked}\n disabled={disabled}\n onCheckedChange={(value) =>\n handleToggle(category.id, value)\n }\n aria-label={`${category.title} cookies`}\n className=\"mt-0.5\"\n />\n </div>\n );\n })}\n </div>\n\n <DialogFooter>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full sm:w-auto\"\n onClick={declineAll}\n >\n Decline Optional\n </Button>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full sm:w-auto\"\n onClick={handleSave}\n >\n Save Preferences\n </Button>\n <Button size=\"sm\" className=\"w-full sm:w-auto\" onClick={acceptAll}>\n Accept All\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\nconst DialogTrigger = DialogPrimitive.Trigger;\nconst DialogPortal = DialogPrimitive.Portal;\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\"fixed inset-0 z-50 bg-black/40\", className)}\n {...props}\n />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-1.5rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-0 rounded-lg border border-border bg-background p-0 shadow-lg outline-none\",\n className,\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className=\"absolute right-3 top-3 rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <X className=\"h-3.5 w-3.5\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nfunction DialogHeader({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\"flex flex-col gap-1 px-4 pt-4 pr-10\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\n \"flex flex-col-reverse gap-1.5 border-t border-border px-4 py-3 sm:flex-row sm:justify-end\",\n className,\n )}\n {...props}\n />\n );\n}\n\nconst DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\n \"text-sm font-semibold tracking-tight text-foreground\",\n className,\n )}\n {...props}\n />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"text-xs leading-relaxed text-muted-foreground\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n};\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\nimport { cn } from \"@/lib/utils\";\n\nconst Switch = React.forwardRef<\n React.ComponentRef<typeof SwitchPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n <SwitchPrimitives.Root\n className={cn(\n \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-muted\",\n className,\n )}\n {...props}\n ref={ref}\n >\n <SwitchPrimitives.Thumb\n className={cn(\n \"pointer-events-none block h-3.5 w-3.5 rounded-full bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5\",\n )}\n />\n </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n","\"use client\";\n\nimport { Settings2 } from \"lucide-react\";\nimport Image from \"next/image\";\nimport { Button } from \"@/components/ui/button\";\nimport cookieImage from \"@/components/crumbie/assets/cookie.png\";\nimport { useCookieConsent } from \"@/hooks/use-cookie-consent\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CookieSettingsButtonProps = {\n className?: string;\n label?: string;\n variant?: \"floating\" | \"inline\";\n};\n\nexport function CookieSettingsButton({\n className,\n label = \"Cookie settings\",\n variant = \"floating\",\n}: CookieSettingsButtonProps) {\n const { hasConsent, isHydrated, openSettings } = useCookieConsent();\n\n if (!isHydrated || !hasConsent) return null;\n\n if (variant === \"inline\") {\n return (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={openSettings}\n className={className}\n aria-label={label}\n >\n <Settings2 className=\"h-3.5 w-3.5\" />\n {label}\n </Button>\n );\n }\n\n return (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={openSettings}\n aria-label={label}\n className={cn(\n \"fixed bottom-3 left-3 z-40 h-9 w-9 rounded-full border-border bg-background shadow-[0_3px_14px_rgb(0,0,0,0.08)] hover:bg-muted sm:bottom-4 sm:left-4\",\n className,\n )}\n >\n <Image\n src={cookieImage}\n alt=\"\"\n width={28}\n height={28}\n className=\"h-7 w-7 object-contain\"\n />\n </Button>\n );\n}\n","\"use client\";\n\nimport { useCookieConsentContext } from \"@/components/crumbie/provider\";\nimport type { CookieCategory } from \"@/types/consent\";\n\nexport function useConsentCategory(category: CookieCategory): boolean {\n const { isAllowed, isHydrated } = useCookieConsentContext();\n if (!isHydrated) return category === \"necessary\";\n return isAllowed(category);\n}\n","\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { useConsentCategory } from \"@/hooks/use-consent-category\";\nimport type { CookieCategory } from \"@/types/consent\";\n\nexport type ConsentGateProps = {\n category: CookieCategory;\n children: ReactNode;\n fallback?: ReactNode;\n};\n\nexport function ConsentGate({\n category,\n children,\n fallback = null,\n}: ConsentGateProps) {\n const allowed = useConsentCategory(category);\n if (!allowed) return <>{fallback}</>;\n return <>{children}</>;\n}\n"],"mappings":"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","names":["e","React","jsx","jsx","React","React","jsx","jsxs","jsx","jsxs","React","jsx","jsx","jsxs","Image","jsx","jsxs","Image","jsx"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crumbie",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Beautiful, accessible cookie consent components for React.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "next dev",
|
|
23
|
+
"build": "next build",
|
|
24
|
+
"build:package": "tsup",
|
|
25
|
+
"prepack": "pnpm build:package",
|
|
26
|
+
"start": "next start",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"format": "prettier --write .",
|
|
29
|
+
"format:check": "prettier --check .",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^19.0.0",
|
|
35
|
+
"react-dom": "^19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@radix-ui/react-dialog": "^1.1.19",
|
|
39
|
+
"@radix-ui/react-slot": "^1.3.0",
|
|
40
|
+
"@radix-ui/react-switch": "^1.3.3",
|
|
41
|
+
"class-variance-authority": "^0.7.1",
|
|
42
|
+
"clsx": "^2.1.1",
|
|
43
|
+
"framer-motion": "^12.42.2",
|
|
44
|
+
"lucide-react": "^1.24.0",
|
|
45
|
+
"next": "16.2.10",
|
|
46
|
+
"next-themes": "^0.4.6",
|
|
47
|
+
"react": "19.2.4",
|
|
48
|
+
"react-dom": "19.2.4",
|
|
49
|
+
"tailwind-merge": "^3.6.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@tailwindcss/postcss": "^4",
|
|
53
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
54
|
+
"@testing-library/react": "^16.3.2",
|
|
55
|
+
"@testing-library/user-event": "^14.6.1",
|
|
56
|
+
"@types/node": "^20",
|
|
57
|
+
"@types/react": "^19",
|
|
58
|
+
"@types/react-dom": "^19",
|
|
59
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
60
|
+
"eslint": "^9",
|
|
61
|
+
"eslint-config-next": "16.2.10",
|
|
62
|
+
"eslint-config-prettier": "^10.1.8",
|
|
63
|
+
"jsdom": "^29.1.1",
|
|
64
|
+
"prettier": "^3.9.5",
|
|
65
|
+
"tailwindcss": "^4",
|
|
66
|
+
"tsup": "^8.5.1",
|
|
67
|
+
"typescript": "^5",
|
|
68
|
+
"vitest": "^4.1.10"
|
|
69
|
+
}
|
|
70
|
+
}
|