react-lgpd-consent 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,9 @@ Solução moderna, acessível e personalizável para gerenciar consentimento de
20
20
  - 🚀 **TypeScript**: API completamente tipada para melhor DX
21
21
  - 📦 **Zero Config**: Funciona out-of-the-box com configurações sensatas
22
22
  - 🎯 **Granular Control**: Controle individual de categorias (analytics, marketing, etc.)
23
+ - 🚫 **Banner Bloqueante**: Modo opcional para exigir interação antes de continuar
24
+ - 🎨 **Sistema de Temas**: Temas customizáveis para integração visual perfeita
25
+ - ⚡ **Carregamento Condicional**: Scripts só executam após consentimento explícito
23
26
 
24
27
  ## 🚀 Instalação
25
28
 
@@ -62,7 +65,10 @@ function Layout() {
62
65
  return (
63
66
  <>
64
67
  <YourContent />
65
- <CookieBanner policyLinkUrl="/politica-privacidade" />
68
+ <CookieBanner
69
+ policyLinkUrl="/politica-privacidade"
70
+ blocking={true} // Padrão: bloqueia até decisão
71
+ />
66
72
  </>
67
73
  )
68
74
  }
@@ -89,7 +95,7 @@ function MyComponent() {
89
95
  ### 4. Carregamento Condicional de Scripts
90
96
 
91
97
  ```tsx
92
- import { ConsentGate, loadScript } from 'react-lgpd-consent'
98
+ import { ConsentGate, loadConditionalScript } from 'react-lgpd-consent'
93
99
 
94
100
  function Analytics() {
95
101
  return (
@@ -99,20 +105,55 @@ function Analytics() {
99
105
  )
100
106
  }
101
107
 
102
- // Ou carregando scripts dinamicamente
108
+ // Ou carregando scripts que aguardam consentimento
103
109
  function MyComponent() {
104
- const { preferences } = useConsent()
110
+ const { preferences, consented } = useConsent()
105
111
 
106
112
  useEffect(() => {
107
- if (preferences.analytics) {
108
- loadScript('ga', 'https://www.googletagmanager.com/gtag/js?id=GA_ID')
113
+ if (consented && preferences.analytics) {
114
+ loadConditionalScript(
115
+ 'ga',
116
+ 'https://www.googletagmanager.com/gtag/js?id=GA_ID',
117
+ () => preferences.analytics, // Condição que aguarda
118
+ )
109
119
  }
110
- }, [preferences.analytics])
120
+ }, [preferences, consented])
111
121
  }
112
122
  ```
113
123
 
114
124
  ## 🎨 Customização
115
125
 
126
+ ### Banner Bloqueante vs Não-bloqueante
127
+
128
+ ```tsx
129
+ // Banner bloqueante (padrão) - impede interação até decisão
130
+ <CookieBanner blocking={true} />
131
+
132
+ // Banner não-intrusivo - permite navegação
133
+ <CookieBanner blocking={false} />
134
+ ```
135
+
136
+ ### Tema Personalizado
137
+
138
+ ```tsx
139
+ import { ConsentProvider, defaultConsentTheme } from 'react-lgpd-consent'
140
+ import { createTheme } from '@mui/material/styles'
141
+
142
+ const meuTema = createTheme({
143
+ ...defaultConsentTheme,
144
+ palette: {
145
+ ...defaultConsentTheme.palette,
146
+ primary: {
147
+ main: '#1976d2', // Sua cor principal
148
+ },
149
+ },
150
+ })
151
+
152
+ <ConsentProvider theme={meuTema}>
153
+ <App />
154
+ </ConsentProvider>
155
+ ```
156
+
116
157
  ### Textos Personalizados
117
158
 
118
159
  ```tsx
@@ -152,16 +193,90 @@ function MyComponent() {
152
193
  >
153
194
  ```
154
195
 
155
- ## 🔧 API Completa
196
+ ## Banner Bloqueante
197
+
198
+ Para cenários onde é necessário bloquear o acesso até obter consentimento explícito:
199
+
200
+ ```tsx
201
+ <CookieBanner blocking />
202
+ ```
203
+
204
+ Com `blocking={true}`, o banner:
205
+
206
+ - Cria um overlay escuro sobre todo o conteúdo
207
+ - Impede interação com o resto da página
208
+ - É útil para casos críticos onde consentimento é obrigatório
209
+
210
+ ## 🎨 Sistema de Temas
211
+
212
+ ### Tema Personalizado
213
+
214
+ ```tsx
215
+ import { createTheme } from '@mui/material/styles'
216
+
217
+ const meuTema = createTheme({
218
+ palette: {
219
+ primary: { main: '#2196f3' },
220
+ secondary: { main: '#f50057' },
221
+ },
222
+ })
223
+
224
+ <ConsentProvider theme={meuTema}>
225
+ <App />
226
+ </ConsentProvider>
227
+ ```
228
+
229
+ ### Tema Padrão
230
+
231
+ O tema padrão do react-lgpd-consent está disponível para customização:
232
+
233
+ ```tsx
234
+ import { defaultConsentTheme } from 'react-lgpd-consent'
235
+
236
+ const temaCustomizado = createTheme({
237
+ ...defaultConsentTheme,
238
+ palette: {
239
+ ...defaultConsentTheme.palette,
240
+ primary: { main: '#your-color' },
241
+ },
242
+ })
243
+ ```
244
+
245
+ ## ⚡ Carregamento Condicional
246
+
247
+ ### Função loadConditionalScript
248
+
249
+ Para scripts que devem aguardar consentimento específico:
250
+
251
+ ```tsx
252
+ import { loadConditionalScript } from 'react-lgpd-consent'
253
+
254
+ // Carrega script apenas quando analytics for aceito
255
+ await loadConditionalScript(
256
+ 'gtag',
257
+ 'https://www.googletagmanager.com/gtag/js?id=GA_ID',
258
+ () => preferences.analytics,
259
+ { timeout: 10000 }, // timeout opcional
260
+ )
261
+ ```
262
+
263
+ ### Parâmetros
264
+
265
+ - `id`: Identificador único para o script
266
+ - `src`: URL do script a ser carregado
267
+ - `condition`: Função que retorna boolean indicando se deve carregar
268
+ - `options`: Configurações opcionais (timeout, etc.)
269
+
270
+ ## �🔧 API Completa
156
271
 
157
272
  ### Components
158
273
 
159
- | Componente | Descrição | Props Principais |
160
- | ------------------ | -------------------------------------- | ------------------------------------------------ |
161
- | `ConsentProvider` | Provider principal do contexto | `initialState`, `texts`, `cookie`, callbacks |
162
- | `CookieBanner` | Banner de consentimento | `policyLinkUrl`, `debug`, pass-through MUI props |
163
- | `PreferencesModal` | Modal de preferências detalhadas | `DialogProps` (MUI pass-through) |
164
- | `ConsentGate` | Renderização condicional por categoria | `category`, `children` |
274
+ | Componente | Descrição | Props Principais |
275
+ | ------------------ | -------------------------------------- | ------------------------------------------------------------ |
276
+ | `ConsentProvider` | Provider principal do contexto | `initialState`, `texts`, `theme`, `cookie`, callbacks |
277
+ | `CookieBanner` | Banner de consentimento | `policyLinkUrl`, `blocking`, `debug`, pass-through MUI props |
278
+ | `PreferencesModal` | Modal de preferências detalhadas | `DialogProps` (MUI pass-through) |
279
+ | `ConsentGate` | Renderização condicional por categoria | `category`, `children` |
165
280
 
166
281
  ### Hook `useConsent()`
167
282
 
@@ -169,6 +284,7 @@ function MyComponent() {
169
284
  interface ConsentContextValue {
170
285
  consented: boolean // usuário já consentiu?
171
286
  preferences: ConsentPreferences // preferências atuais
287
+ isModalOpen: boolean // estado do modal de preferências
172
288
  acceptAll(): void // aceitar todas as categorias
173
289
  rejectAll(): void // recusar opcionais
174
290
  setPreference(cat: Category, value: boolean): void // definir categoria específica
@@ -178,9 +294,19 @@ interface ConsentContextValue {
178
294
  }
179
295
  ```
180
296
 
297
+ ### Hook `useConsentTexts()`
298
+
299
+ ```typescript
300
+ // Acesso aos textos contextuais
301
+ const texts = useConsentTexts()
302
+ console.log(texts.banner.title) // "Política de Cookies"
303
+ ```
304
+
181
305
  ### Utilitários
182
306
 
183
307
  - `loadScript(id, src, attrs?)` - Carrega scripts dinamicamente
308
+ - `loadConditionalScript(id, src, condition, options?)` - Carrega scripts condicionalmente
309
+ - `defaultConsentTheme` - Tema padrão do Material-UI
184
310
  - Tipos TypeScript completos exportados
185
311
 
186
312
  ## 🌐 SSR / Next.js
package/dist/index.cjs CHANGED
@@ -34,6 +34,8 @@ __export(index_exports, {
34
34
  ConsentProvider: () => ConsentProvider,
35
35
  CookieBanner: () => CookieBanner,
36
36
  PreferencesModal: () => PreferencesModal,
37
+ defaultConsentTheme: () => defaultConsentTheme,
38
+ loadConditionalScript: () => loadConditionalScript,
37
39
  loadScript: () => loadScript,
38
40
  useConsent: () => useConsent,
39
41
  useConsentTexts: () => useConsentTexts
@@ -42,6 +44,7 @@ module.exports = __toCommonJS(index_exports);
42
44
 
43
45
  // src/components/CookieBanner.tsx
44
46
  var import_Button = __toESM(require("@mui/material/Button"), 1);
47
+ var import_Box = __toESM(require("@mui/material/Box"), 1);
45
48
  var import_Link = __toESM(require("@mui/material/Link"), 1);
46
49
  var import_Paper = __toESM(require("@mui/material/Paper"), 1);
47
50
  var import_Snackbar = __toESM(require("@mui/material/Snackbar"), 1);
@@ -50,6 +53,7 @@ var import_Typography = __toESM(require("@mui/material/Typography"), 1);
50
53
 
51
54
  // src/context/ConsentContext.tsx
52
55
  var React = __toESM(require("react"), 1);
56
+ var import_styles2 = require("@mui/material/styles");
53
57
 
54
58
  // src/utils/cookieUtils.ts
55
59
  var import_js_cookie = __toESM(require("js-cookie"), 1);
@@ -86,6 +90,77 @@ function removeConsentCookie(opts) {
86
90
  import_js_cookie.default.remove(o.name, { path: o.path });
87
91
  }
88
92
 
93
+ // src/utils/theme.ts
94
+ var import_styles = require("@mui/material/styles");
95
+ var defaultConsentTheme = (0, import_styles.createTheme)({
96
+ palette: {
97
+ primary: {
98
+ main: "#1976d2",
99
+ // Azul institucional
100
+ contrastText: "#ffffff"
101
+ },
102
+ secondary: {
103
+ main: "#dc004e",
104
+ // Rosa/vermelho para ações importantes
105
+ contrastText: "#ffffff"
106
+ },
107
+ background: {
108
+ default: "#fafafa",
109
+ paper: "#ffffff"
110
+ },
111
+ text: {
112
+ primary: "#333333",
113
+ secondary: "#666666"
114
+ },
115
+ action: {
116
+ hover: "rgba(25, 118, 210, 0.04)"
117
+ }
118
+ },
119
+ typography: {
120
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
121
+ body2: {
122
+ fontSize: "0.875rem",
123
+ lineHeight: 1.43
124
+ },
125
+ button: {
126
+ fontWeight: 500,
127
+ textTransform: "none"
128
+ // Manter capitalização original
129
+ }
130
+ },
131
+ components: {
132
+ MuiButton: {
133
+ styleOverrides: {
134
+ root: {
135
+ borderRadius: 8,
136
+ paddingX: 16,
137
+ paddingY: 8
138
+ },
139
+ contained: {
140
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
141
+ "&:hover": {
142
+ boxShadow: "0 4px 8px rgba(0,0,0,0.15)"
143
+ }
144
+ }
145
+ }
146
+ },
147
+ MuiPaper: {
148
+ styleOverrides: {
149
+ root: {
150
+ borderRadius: 12
151
+ }
152
+ }
153
+ },
154
+ MuiDialog: {
155
+ styleOverrides: {
156
+ paper: {
157
+ borderRadius: 16
158
+ }
159
+ }
160
+ }
161
+ }
162
+ });
163
+
89
164
  // src/context/ConsentContext.tsx
90
165
  var import_jsx_runtime = require("react/jsx-runtime");
91
166
  var DEFAULT_PREFERENCES = {
@@ -148,6 +223,7 @@ var TextsCtx = React.createContext(DEFAULT_TEXTS);
148
223
  function ConsentProvider({
149
224
  initialState,
150
225
  texts: textsProp,
226
+ theme,
151
227
  onConsentGiven,
152
228
  onPreferencesSaved,
153
229
  cookie: cookieOpts,
@@ -161,6 +237,10 @@ function ConsentProvider({
161
237
  () => ({ ...DEFAULT_COOKIE_OPTS, ...cookieOpts ?? {} }),
162
238
  [cookieOpts]
163
239
  );
240
+ const appliedTheme = React.useMemo(
241
+ () => theme || defaultConsentTheme,
242
+ [theme]
243
+ );
164
244
  const boot = React.useMemo(() => {
165
245
  if (initialState) return { ...initialState, isModalOpen: false };
166
246
  const saved = readConsentCookie(cookie.name);
@@ -176,14 +256,15 @@ function ConsentProvider({
176
256
  }, [state, cookie]);
177
257
  const prevConsented = React.useRef(state.consented);
178
258
  React.useEffect(() => {
179
- if (!prevConsented.current && state.consented && onConsentGiven)
180
- onConsentGiven(state);
259
+ if (!prevConsented.current && state.consented && onConsentGiven) {
260
+ setTimeout(() => onConsentGiven(state), 150);
261
+ }
181
262
  prevConsented.current = state.consented;
182
263
  }, [state, onConsentGiven]);
183
264
  const prevPrefs = React.useRef(state.preferences);
184
265
  React.useEffect(() => {
185
266
  if (state.consented && onPreferencesSaved && prevPrefs.current !== state.preferences) {
186
- onPreferencesSaved(state.preferences);
267
+ setTimeout(() => onPreferencesSaved(state.preferences), 150);
187
268
  prevPrefs.current = state.preferences;
188
269
  }
189
270
  }, [state, onPreferencesSaved]);
@@ -200,6 +281,7 @@ function ConsentProvider({
200
281
  return {
201
282
  consented: !!state.consented,
202
283
  preferences: state.preferences,
284
+ isModalOpen: state.isModalOpen,
203
285
  acceptAll,
204
286
  rejectAll,
205
287
  setPreference,
@@ -208,7 +290,7 @@ function ConsentProvider({
208
290
  resetConsent
209
291
  };
210
292
  }, [state, cookie]);
211
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StateCtx.Provider, { value: state, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsCtx.Provider, { value: api, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextsCtx.Provider, { value: texts, children }) }) });
293
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_styles2.ThemeProvider, { theme: appliedTheme, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StateCtx.Provider, { value: state, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsCtx.Provider, { value: api, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextsCtx.Provider, { value: texts, children }) }) }) });
212
294
  }
213
295
  function useConsentStateInternal() {
214
296
  const ctx = React.useContext(StateCtx);
@@ -234,6 +316,7 @@ function useConsent() {
234
316
  return {
235
317
  consented: state.consented,
236
318
  preferences: state.preferences,
319
+ isModalOpen: state.isModalOpen,
237
320
  acceptAll: actions.acceptAll,
238
321
  rejectAll: actions.rejectAll,
239
322
  setPreference: actions.setPreference,
@@ -251,6 +334,8 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
251
334
  function CookieBanner({
252
335
  policyLinkUrl,
253
336
  debug,
337
+ blocking = true,
338
+ // Por padrão, bloqueia até decisão
254
339
  SnackbarProps,
255
340
  PaperProps
256
341
  }) {
@@ -258,49 +343,84 @@ function CookieBanner({
258
343
  const texts = useConsentTexts();
259
344
  const open = debug ? true : !consented;
260
345
  if (!open) return null;
346
+ const bannerContent = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
347
+ import_Paper.default,
348
+ {
349
+ elevation: 3,
350
+ sx: { p: 2, maxWidth: 720, mx: "auto" },
351
+ ...PaperProps,
352
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack.default, { spacing: 1, children: [
353
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Typography.default, { variant: "body2", children: [
354
+ texts.bannerMessage,
355
+ " ",
356
+ policyLinkUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
357
+ import_Link.default,
358
+ {
359
+ href: policyLinkUrl,
360
+ underline: "hover",
361
+ target: "_blank",
362
+ rel: "noopener noreferrer",
363
+ children: texts.policyLink ?? "Saiba mais"
364
+ }
365
+ )
366
+ ] }),
367
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
368
+ import_Stack.default,
369
+ {
370
+ direction: { xs: "column", sm: "row" },
371
+ spacing: 1,
372
+ justifyContent: "flex-end",
373
+ children: [
374
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Button.default, { variant: "outlined", onClick: rejectAll, children: texts.declineAll }),
375
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Button.default, { variant: "contained", onClick: acceptAll, children: texts.acceptAll }),
376
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Button.default, { variant: "text", onClick: openPreferences, children: texts.preferences })
377
+ ]
378
+ }
379
+ )
380
+ ] })
381
+ }
382
+ );
383
+ if (blocking) {
384
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
385
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
386
+ import_Box.default,
387
+ {
388
+ sx: {
389
+ position: "fixed",
390
+ top: 0,
391
+ left: 0,
392
+ right: 0,
393
+ bottom: 0,
394
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
395
+ zIndex: 1299
396
+ // Abaixo do banner mas acima do conteúdo
397
+ }
398
+ }
399
+ ),
400
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
401
+ import_Box.default,
402
+ {
403
+ sx: {
404
+ position: "fixed",
405
+ bottom: 0,
406
+ left: 0,
407
+ right: 0,
408
+ zIndex: 1300,
409
+ // Acima do overlay
410
+ p: 2
411
+ },
412
+ children: bannerContent
413
+ }
414
+ )
415
+ ] });
416
+ }
261
417
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
262
418
  import_Snackbar.default,
263
419
  {
264
420
  open,
265
421
  anchorOrigin: { vertical: "bottom", horizontal: "center" },
266
422
  ...SnackbarProps,
267
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
268
- import_Paper.default,
269
- {
270
- elevation: 3,
271
- sx: { p: 2, maxWidth: 720, mx: "auto" },
272
- ...PaperProps,
273
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack.default, { spacing: 1, children: [
274
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Typography.default, { variant: "body2", children: [
275
- texts.bannerMessage,
276
- " ",
277
- policyLinkUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
278
- import_Link.default,
279
- {
280
- href: policyLinkUrl,
281
- underline: "hover",
282
- target: "_blank",
283
- rel: "noopener noreferrer",
284
- children: texts.policyLink ?? "Saiba mais"
285
- }
286
- )
287
- ] }),
288
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
289
- import_Stack.default,
290
- {
291
- direction: { xs: "column", sm: "row" },
292
- spacing: 1,
293
- justifyContent: "flex-end",
294
- children: [
295
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Button.default, { variant: "outlined", onClick: rejectAll, children: texts.declineAll }),
296
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Button.default, { variant: "contained", onClick: acceptAll, children: texts.acceptAll }),
297
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Button.default, { variant: "text", onClick: openPreferences, children: texts.preferences })
298
- ]
299
- }
300
- )
301
- ] })
302
- }
303
- )
423
+ children: bannerContent
304
424
  }
305
425
  );
306
426
  }
@@ -319,9 +439,9 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
319
439
  function PreferencesModal({
320
440
  DialogProps: DialogProps2
321
441
  }) {
322
- const { preferences, setPreference, closePreferences } = useConsent();
442
+ const { preferences, setPreference, closePreferences, isModalOpen } = useConsent();
323
443
  const texts = useConsentTexts();
324
- const open = (DialogProps2 && "open" in DialogProps2 ? DialogProps2.open : void 0) ?? true;
444
+ const open = DialogProps2?.open ?? isModalOpen ?? false;
325
445
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
326
446
  import_Dialog.default,
327
447
  {
@@ -394,12 +514,34 @@ function loadScript(id, src, attrs = {}) {
394
514
  for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v);
395
515
  document.body.appendChild(s);
396
516
  }
517
+ function loadConditionalScript(id, src, condition, attrs = {}, maxWaitMs = 5e3) {
518
+ if (typeof document === "undefined") return Promise.resolve();
519
+ if (document.getElementById(id)) return Promise.resolve();
520
+ return new Promise((resolve, reject) => {
521
+ const startTime = Date.now();
522
+ const checkCondition = () => {
523
+ if (condition()) {
524
+ loadScript(id, src, attrs);
525
+ resolve();
526
+ } else if (Date.now() - startTime > maxWaitMs) {
527
+ reject(
528
+ new Error(`Timeout waiting for consent condition for script ${id}`)
529
+ );
530
+ } else {
531
+ setTimeout(checkCondition, 100);
532
+ }
533
+ };
534
+ checkCondition();
535
+ });
536
+ }
397
537
  // Annotate the CommonJS export names for ESM import in node:
398
538
  0 && (module.exports = {
399
539
  ConsentGate,
400
540
  ConsentProvider,
401
541
  CookieBanner,
402
542
  PreferencesModal,
543
+ defaultConsentTheme,
544
+ loadConditionalScript,
403
545
  loadScript,
404
546
  useConsent,
405
547
  useConsentTexts
package/dist/index.d.cts CHANGED
@@ -3,14 +3,17 @@ import { PaperProps } from '@mui/material/Paper';
3
3
  import { SnackbarProps } from '@mui/material/Snackbar';
4
4
  import { DialogProps } from '@mui/material/Dialog';
5
5
  import * as React$1 from 'react';
6
+ import * as _mui_material_styles from '@mui/material/styles';
6
7
 
7
8
  interface CookieBannerProps {
8
9
  policyLinkUrl?: string;
9
10
  debug?: boolean;
11
+ blocking?: boolean;
10
12
  SnackbarProps?: Partial<SnackbarProps>;
11
13
  PaperProps?: Partial<PaperProps>;
12
14
  }
13
- declare function CookieBanner({ policyLinkUrl, debug, SnackbarProps, PaperProps, }: Readonly<CookieBannerProps>): react_jsx_runtime.JSX.Element | null;
15
+ declare function CookieBanner({ policyLinkUrl, debug, blocking, // Por padrão, bloqueia até decisão
16
+ SnackbarProps, PaperProps, }: Readonly<CookieBannerProps>): react_jsx_runtime.JSX.Element | null;
14
17
 
15
18
  interface PreferencesModalProps {
16
19
  DialogProps?: Partial<DialogProps>;
@@ -75,6 +78,8 @@ interface ConsentProviderProps {
75
78
  initialState?: ConsentState;
76
79
  /** Textos customizados para a interface. */
77
80
  texts?: Partial<ConsentTexts>;
81
+ /** Tema customizado para os componentes MUI. */
82
+ theme?: any;
78
83
  /** Callback chamado quando o consentimento é dado. */
79
84
  onConsentGiven?: (state: ConsentState) => void;
80
85
  /** Callback chamado ao salvar preferências. */
@@ -92,6 +97,8 @@ interface ConsentContextValue {
92
97
  consented: boolean;
93
98
  /** Preferências atuais do usuário. */
94
99
  preferences: ConsentPreferences;
100
+ /** Indica se o modal de preferências está aberto. */
101
+ isModalOpen?: boolean;
95
102
  /** Aceita todas as categorias de consentimento. */
96
103
  acceptAll: () => void;
97
104
  /** Rejeita todas as categorias de consentimento. */
@@ -106,7 +113,7 @@ interface ConsentContextValue {
106
113
  resetConsent: () => void;
107
114
  }
108
115
 
109
- declare function ConsentProvider({ initialState, texts: textsProp, onConsentGiven, onPreferencesSaved, cookie: cookieOpts, children, }: Readonly<ConsentProviderProps>): react_jsx_runtime.JSX.Element;
116
+ declare function ConsentProvider({ initialState, texts: textsProp, theme, onConsentGiven, onPreferencesSaved, cookie: cookieOpts, children, }: Readonly<ConsentProviderProps>): react_jsx_runtime.JSX.Element;
110
117
 
111
118
  declare function useConsent(): ConsentContextValue;
112
119
  /**
@@ -120,6 +127,21 @@ declare function ConsentGate(props: Readonly<{
120
127
  children: React$1.ReactNode;
121
128
  }>): react_jsx_runtime.JSX.Element | null;
122
129
 
130
+ /**
131
+ * Carrega um script dinamicamente após verificação de consentimento.
132
+ * Recomenda-se usar com ConsentGate para melhor controle.
133
+ */
123
134
  declare function loadScript(id: string, src: string, attrs?: Record<string, string>): void;
135
+ /**
136
+ * Carrega script condicionalmente baseado no consentimento.
137
+ * Aguarda o consentimento ser dado antes de executar.
138
+ */
139
+ declare function loadConditionalScript(id: string, src: string, condition: () => boolean, attrs?: Record<string, string>, maxWaitMs?: number): Promise<void>;
140
+
141
+ /**
142
+ * Tema padrão para os componentes de consentimento.
143
+ * Baseado no design system da ANPD/governo brasileiro.
144
+ */
145
+ declare const defaultConsentTheme: _mui_material_styles.Theme;
124
146
 
125
- export { type Category, type ConsentCookieOptions, ConsentGate, type ConsentPreferences, ConsentProvider, type ConsentState, type ConsentTexts, CookieBanner, PreferencesModal, loadScript, useConsent, useConsentTexts };
147
+ export { type Category, type ConsentCookieOptions, ConsentGate, type ConsentPreferences, ConsentProvider, type ConsentState, type ConsentTexts, CookieBanner, PreferencesModal, defaultConsentTheme, loadConditionalScript, loadScript, useConsent, useConsentTexts };
package/dist/index.d.ts CHANGED
@@ -3,14 +3,17 @@ import { PaperProps } from '@mui/material/Paper';
3
3
  import { SnackbarProps } from '@mui/material/Snackbar';
4
4
  import { DialogProps } from '@mui/material/Dialog';
5
5
  import * as React$1 from 'react';
6
+ import * as _mui_material_styles from '@mui/material/styles';
6
7
 
7
8
  interface CookieBannerProps {
8
9
  policyLinkUrl?: string;
9
10
  debug?: boolean;
11
+ blocking?: boolean;
10
12
  SnackbarProps?: Partial<SnackbarProps>;
11
13
  PaperProps?: Partial<PaperProps>;
12
14
  }
13
- declare function CookieBanner({ policyLinkUrl, debug, SnackbarProps, PaperProps, }: Readonly<CookieBannerProps>): react_jsx_runtime.JSX.Element | null;
15
+ declare function CookieBanner({ policyLinkUrl, debug, blocking, // Por padrão, bloqueia até decisão
16
+ SnackbarProps, PaperProps, }: Readonly<CookieBannerProps>): react_jsx_runtime.JSX.Element | null;
14
17
 
15
18
  interface PreferencesModalProps {
16
19
  DialogProps?: Partial<DialogProps>;
@@ -75,6 +78,8 @@ interface ConsentProviderProps {
75
78
  initialState?: ConsentState;
76
79
  /** Textos customizados para a interface. */
77
80
  texts?: Partial<ConsentTexts>;
81
+ /** Tema customizado para os componentes MUI. */
82
+ theme?: any;
78
83
  /** Callback chamado quando o consentimento é dado. */
79
84
  onConsentGiven?: (state: ConsentState) => void;
80
85
  /** Callback chamado ao salvar preferências. */
@@ -92,6 +97,8 @@ interface ConsentContextValue {
92
97
  consented: boolean;
93
98
  /** Preferências atuais do usuário. */
94
99
  preferences: ConsentPreferences;
100
+ /** Indica se o modal de preferências está aberto. */
101
+ isModalOpen?: boolean;
95
102
  /** Aceita todas as categorias de consentimento. */
96
103
  acceptAll: () => void;
97
104
  /** Rejeita todas as categorias de consentimento. */
@@ -106,7 +113,7 @@ interface ConsentContextValue {
106
113
  resetConsent: () => void;
107
114
  }
108
115
 
109
- declare function ConsentProvider({ initialState, texts: textsProp, onConsentGiven, onPreferencesSaved, cookie: cookieOpts, children, }: Readonly<ConsentProviderProps>): react_jsx_runtime.JSX.Element;
116
+ declare function ConsentProvider({ initialState, texts: textsProp, theme, onConsentGiven, onPreferencesSaved, cookie: cookieOpts, children, }: Readonly<ConsentProviderProps>): react_jsx_runtime.JSX.Element;
110
117
 
111
118
  declare function useConsent(): ConsentContextValue;
112
119
  /**
@@ -120,6 +127,21 @@ declare function ConsentGate(props: Readonly<{
120
127
  children: React$1.ReactNode;
121
128
  }>): react_jsx_runtime.JSX.Element | null;
122
129
 
130
+ /**
131
+ * Carrega um script dinamicamente após verificação de consentimento.
132
+ * Recomenda-se usar com ConsentGate para melhor controle.
133
+ */
123
134
  declare function loadScript(id: string, src: string, attrs?: Record<string, string>): void;
135
+ /**
136
+ * Carrega script condicionalmente baseado no consentimento.
137
+ * Aguarda o consentimento ser dado antes de executar.
138
+ */
139
+ declare function loadConditionalScript(id: string, src: string, condition: () => boolean, attrs?: Record<string, string>, maxWaitMs?: number): Promise<void>;
140
+
141
+ /**
142
+ * Tema padrão para os componentes de consentimento.
143
+ * Baseado no design system da ANPD/governo brasileiro.
144
+ */
145
+ declare const defaultConsentTheme: _mui_material_styles.Theme;
124
146
 
125
- export { type Category, type ConsentCookieOptions, ConsentGate, type ConsentPreferences, ConsentProvider, type ConsentState, type ConsentTexts, CookieBanner, PreferencesModal, loadScript, useConsent, useConsentTexts };
147
+ export { type Category, type ConsentCookieOptions, ConsentGate, type ConsentPreferences, ConsentProvider, type ConsentState, type ConsentTexts, CookieBanner, PreferencesModal, defaultConsentTheme, loadConditionalScript, loadScript, useConsent, useConsentTexts };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/components/CookieBanner.tsx
2
2
  import Button from "@mui/material/Button";
3
+ import Box from "@mui/material/Box";
3
4
  import Link from "@mui/material/Link";
4
5
  import Paper from "@mui/material/Paper";
5
6
  import Snackbar from "@mui/material/Snackbar";
@@ -8,6 +9,7 @@ import Typography from "@mui/material/Typography";
8
9
 
9
10
  // src/context/ConsentContext.tsx
10
11
  import * as React from "react";
12
+ import { ThemeProvider } from "@mui/material/styles";
11
13
 
12
14
  // src/utils/cookieUtils.ts
13
15
  import Cookies from "js-cookie";
@@ -44,6 +46,77 @@ function removeConsentCookie(opts) {
44
46
  Cookies.remove(o.name, { path: o.path });
45
47
  }
46
48
 
49
+ // src/utils/theme.ts
50
+ import { createTheme } from "@mui/material/styles";
51
+ var defaultConsentTheme = createTheme({
52
+ palette: {
53
+ primary: {
54
+ main: "#1976d2",
55
+ // Azul institucional
56
+ contrastText: "#ffffff"
57
+ },
58
+ secondary: {
59
+ main: "#dc004e",
60
+ // Rosa/vermelho para ações importantes
61
+ contrastText: "#ffffff"
62
+ },
63
+ background: {
64
+ default: "#fafafa",
65
+ paper: "#ffffff"
66
+ },
67
+ text: {
68
+ primary: "#333333",
69
+ secondary: "#666666"
70
+ },
71
+ action: {
72
+ hover: "rgba(25, 118, 210, 0.04)"
73
+ }
74
+ },
75
+ typography: {
76
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
77
+ body2: {
78
+ fontSize: "0.875rem",
79
+ lineHeight: 1.43
80
+ },
81
+ button: {
82
+ fontWeight: 500,
83
+ textTransform: "none"
84
+ // Manter capitalização original
85
+ }
86
+ },
87
+ components: {
88
+ MuiButton: {
89
+ styleOverrides: {
90
+ root: {
91
+ borderRadius: 8,
92
+ paddingX: 16,
93
+ paddingY: 8
94
+ },
95
+ contained: {
96
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
97
+ "&:hover": {
98
+ boxShadow: "0 4px 8px rgba(0,0,0,0.15)"
99
+ }
100
+ }
101
+ }
102
+ },
103
+ MuiPaper: {
104
+ styleOverrides: {
105
+ root: {
106
+ borderRadius: 12
107
+ }
108
+ }
109
+ },
110
+ MuiDialog: {
111
+ styleOverrides: {
112
+ paper: {
113
+ borderRadius: 16
114
+ }
115
+ }
116
+ }
117
+ }
118
+ });
119
+
47
120
  // src/context/ConsentContext.tsx
48
121
  import { jsx } from "react/jsx-runtime";
49
122
  var DEFAULT_PREFERENCES = {
@@ -106,6 +179,7 @@ var TextsCtx = React.createContext(DEFAULT_TEXTS);
106
179
  function ConsentProvider({
107
180
  initialState,
108
181
  texts: textsProp,
182
+ theme,
109
183
  onConsentGiven,
110
184
  onPreferencesSaved,
111
185
  cookie: cookieOpts,
@@ -119,6 +193,10 @@ function ConsentProvider({
119
193
  () => ({ ...DEFAULT_COOKIE_OPTS, ...cookieOpts ?? {} }),
120
194
  [cookieOpts]
121
195
  );
196
+ const appliedTheme = React.useMemo(
197
+ () => theme || defaultConsentTheme,
198
+ [theme]
199
+ );
122
200
  const boot = React.useMemo(() => {
123
201
  if (initialState) return { ...initialState, isModalOpen: false };
124
202
  const saved = readConsentCookie(cookie.name);
@@ -134,14 +212,15 @@ function ConsentProvider({
134
212
  }, [state, cookie]);
135
213
  const prevConsented = React.useRef(state.consented);
136
214
  React.useEffect(() => {
137
- if (!prevConsented.current && state.consented && onConsentGiven)
138
- onConsentGiven(state);
215
+ if (!prevConsented.current && state.consented && onConsentGiven) {
216
+ setTimeout(() => onConsentGiven(state), 150);
217
+ }
139
218
  prevConsented.current = state.consented;
140
219
  }, [state, onConsentGiven]);
141
220
  const prevPrefs = React.useRef(state.preferences);
142
221
  React.useEffect(() => {
143
222
  if (state.consented && onPreferencesSaved && prevPrefs.current !== state.preferences) {
144
- onPreferencesSaved(state.preferences);
223
+ setTimeout(() => onPreferencesSaved(state.preferences), 150);
145
224
  prevPrefs.current = state.preferences;
146
225
  }
147
226
  }, [state, onPreferencesSaved]);
@@ -158,6 +237,7 @@ function ConsentProvider({
158
237
  return {
159
238
  consented: !!state.consented,
160
239
  preferences: state.preferences,
240
+ isModalOpen: state.isModalOpen,
161
241
  acceptAll,
162
242
  rejectAll,
163
243
  setPreference,
@@ -166,7 +246,7 @@ function ConsentProvider({
166
246
  resetConsent
167
247
  };
168
248
  }, [state, cookie]);
169
- return /* @__PURE__ */ jsx(StateCtx.Provider, { value: state, children: /* @__PURE__ */ jsx(ActionsCtx.Provider, { value: api, children: /* @__PURE__ */ jsx(TextsCtx.Provider, { value: texts, children }) }) });
249
+ return /* @__PURE__ */ jsx(ThemeProvider, { theme: appliedTheme, children: /* @__PURE__ */ jsx(StateCtx.Provider, { value: state, children: /* @__PURE__ */ jsx(ActionsCtx.Provider, { value: api, children: /* @__PURE__ */ jsx(TextsCtx.Provider, { value: texts, children }) }) }) });
170
250
  }
171
251
  function useConsentStateInternal() {
172
252
  const ctx = React.useContext(StateCtx);
@@ -192,6 +272,7 @@ function useConsent() {
192
272
  return {
193
273
  consented: state.consented,
194
274
  preferences: state.preferences,
275
+ isModalOpen: state.isModalOpen,
195
276
  acceptAll: actions.acceptAll,
196
277
  rejectAll: actions.rejectAll,
197
278
  setPreference: actions.setPreference,
@@ -205,10 +286,12 @@ function useConsentTexts() {
205
286
  }
206
287
 
207
288
  // src/components/CookieBanner.tsx
208
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
289
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
209
290
  function CookieBanner({
210
291
  policyLinkUrl,
211
292
  debug,
293
+ blocking = true,
294
+ // Por padrão, bloqueia até decisão
212
295
  SnackbarProps,
213
296
  PaperProps
214
297
  }) {
@@ -216,49 +299,84 @@ function CookieBanner({
216
299
  const texts = useConsentTexts();
217
300
  const open = debug ? true : !consented;
218
301
  if (!open) return null;
302
+ const bannerContent = /* @__PURE__ */ jsx2(
303
+ Paper,
304
+ {
305
+ elevation: 3,
306
+ sx: { p: 2, maxWidth: 720, mx: "auto" },
307
+ ...PaperProps,
308
+ children: /* @__PURE__ */ jsxs(Stack, { spacing: 1, children: [
309
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [
310
+ texts.bannerMessage,
311
+ " ",
312
+ policyLinkUrl && /* @__PURE__ */ jsx2(
313
+ Link,
314
+ {
315
+ href: policyLinkUrl,
316
+ underline: "hover",
317
+ target: "_blank",
318
+ rel: "noopener noreferrer",
319
+ children: texts.policyLink ?? "Saiba mais"
320
+ }
321
+ )
322
+ ] }),
323
+ /* @__PURE__ */ jsxs(
324
+ Stack,
325
+ {
326
+ direction: { xs: "column", sm: "row" },
327
+ spacing: 1,
328
+ justifyContent: "flex-end",
329
+ children: [
330
+ /* @__PURE__ */ jsx2(Button, { variant: "outlined", onClick: rejectAll, children: texts.declineAll }),
331
+ /* @__PURE__ */ jsx2(Button, { variant: "contained", onClick: acceptAll, children: texts.acceptAll }),
332
+ /* @__PURE__ */ jsx2(Button, { variant: "text", onClick: openPreferences, children: texts.preferences })
333
+ ]
334
+ }
335
+ )
336
+ ] })
337
+ }
338
+ );
339
+ if (blocking) {
340
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
341
+ /* @__PURE__ */ jsx2(
342
+ Box,
343
+ {
344
+ sx: {
345
+ position: "fixed",
346
+ top: 0,
347
+ left: 0,
348
+ right: 0,
349
+ bottom: 0,
350
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
351
+ zIndex: 1299
352
+ // Abaixo do banner mas acima do conteúdo
353
+ }
354
+ }
355
+ ),
356
+ /* @__PURE__ */ jsx2(
357
+ Box,
358
+ {
359
+ sx: {
360
+ position: "fixed",
361
+ bottom: 0,
362
+ left: 0,
363
+ right: 0,
364
+ zIndex: 1300,
365
+ // Acima do overlay
366
+ p: 2
367
+ },
368
+ children: bannerContent
369
+ }
370
+ )
371
+ ] });
372
+ }
219
373
  return /* @__PURE__ */ jsx2(
220
374
  Snackbar,
221
375
  {
222
376
  open,
223
377
  anchorOrigin: { vertical: "bottom", horizontal: "center" },
224
378
  ...SnackbarProps,
225
- children: /* @__PURE__ */ jsx2(
226
- Paper,
227
- {
228
- elevation: 3,
229
- sx: { p: 2, maxWidth: 720, mx: "auto" },
230
- ...PaperProps,
231
- children: /* @__PURE__ */ jsxs(Stack, { spacing: 1, children: [
232
- /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [
233
- texts.bannerMessage,
234
- " ",
235
- policyLinkUrl && /* @__PURE__ */ jsx2(
236
- Link,
237
- {
238
- href: policyLinkUrl,
239
- underline: "hover",
240
- target: "_blank",
241
- rel: "noopener noreferrer",
242
- children: texts.policyLink ?? "Saiba mais"
243
- }
244
- )
245
- ] }),
246
- /* @__PURE__ */ jsxs(
247
- Stack,
248
- {
249
- direction: { xs: "column", sm: "row" },
250
- spacing: 1,
251
- justifyContent: "flex-end",
252
- children: [
253
- /* @__PURE__ */ jsx2(Button, { variant: "outlined", onClick: rejectAll, children: texts.declineAll }),
254
- /* @__PURE__ */ jsx2(Button, { variant: "contained", onClick: acceptAll, children: texts.acceptAll }),
255
- /* @__PURE__ */ jsx2(Button, { variant: "text", onClick: openPreferences, children: texts.preferences })
256
- ]
257
- }
258
- )
259
- ] })
260
- }
261
- )
379
+ children: bannerContent
262
380
  }
263
381
  );
264
382
  }
@@ -277,9 +395,9 @@ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
277
395
  function PreferencesModal({
278
396
  DialogProps: DialogProps2
279
397
  }) {
280
- const { preferences, setPreference, closePreferences } = useConsent();
398
+ const { preferences, setPreference, closePreferences, isModalOpen } = useConsent();
281
399
  const texts = useConsentTexts();
282
- const open = (DialogProps2 && "open" in DialogProps2 ? DialogProps2.open : void 0) ?? true;
400
+ const open = DialogProps2?.open ?? isModalOpen ?? false;
283
401
  return /* @__PURE__ */ jsxs2(
284
402
  Dialog,
285
403
  {
@@ -334,11 +452,11 @@ function PreferencesModal({
334
452
  }
335
453
 
336
454
  // src/utils/ConsentGate.tsx
337
- import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
455
+ import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
338
456
  function ConsentGate(props) {
339
457
  const { preferences } = useConsent();
340
458
  if (!preferences[props.category]) return null;
341
- return /* @__PURE__ */ jsx4(Fragment, { children: props.children });
459
+ return /* @__PURE__ */ jsx4(Fragment2, { children: props.children });
342
460
  }
343
461
 
344
462
  // src/utils/scriptLoader.ts
@@ -352,11 +470,33 @@ function loadScript(id, src, attrs = {}) {
352
470
  for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v);
353
471
  document.body.appendChild(s);
354
472
  }
473
+ function loadConditionalScript(id, src, condition, attrs = {}, maxWaitMs = 5e3) {
474
+ if (typeof document === "undefined") return Promise.resolve();
475
+ if (document.getElementById(id)) return Promise.resolve();
476
+ return new Promise((resolve, reject) => {
477
+ const startTime = Date.now();
478
+ const checkCondition = () => {
479
+ if (condition()) {
480
+ loadScript(id, src, attrs);
481
+ resolve();
482
+ } else if (Date.now() - startTime > maxWaitMs) {
483
+ reject(
484
+ new Error(`Timeout waiting for consent condition for script ${id}`)
485
+ );
486
+ } else {
487
+ setTimeout(checkCondition, 100);
488
+ }
489
+ };
490
+ checkCondition();
491
+ });
492
+ }
355
493
  export {
356
494
  ConsentGate,
357
495
  ConsentProvider,
358
496
  CookieBanner,
359
497
  PreferencesModal,
498
+ defaultConsentTheme,
499
+ loadConditionalScript,
360
500
  loadScript,
361
501
  useConsent,
362
502
  useConsentTexts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-lgpd-consent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Biblioteca de consentimento de cookies (LGPD) para React e Next.js, com contexto, banner e modal personalizáveis usando MUI.",
5
5
  "keywords": [
6
6
  "lgpd",