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 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
+ }
@@ -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
+ }