@redacto.io/consent-sdk-react 5.3.0 → 7.0.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.
Files changed (68) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/CHANGELOG.md +23 -0
  3. package/dist/privacy-center.d.mts +100 -42
  4. package/dist/privacy-center.d.ts +100 -42
  5. package/dist/privacy-center.js +4232 -2247
  6. package/dist/privacy-center.mjs +4266 -2284
  7. package/package.json +1 -1
  8. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -10
  9. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +18 -17
  10. package/src/RedactoPrivacyCenter/api/actions.ts +52 -8
  11. package/src/RedactoPrivacyCenter/api/client.ts +58 -15
  12. package/src/RedactoPrivacyCenter/api/orgBranding.ts +9 -0
  13. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +729 -278
  14. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +187 -0
  15. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +65 -165
  16. package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +179 -0
  17. package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +318 -0
  18. package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +333 -0
  19. package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +102 -0
  20. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +6 -2
  21. package/src/RedactoPrivacyCenter/components/Form.tsx +3 -3
  22. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +9 -6
  23. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +1 -1
  24. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +101 -56
  25. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +16 -2
  26. package/src/RedactoPrivacyCenter/components/types.ts +2 -5
  27. package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +44 -0
  28. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +21 -1
  29. package/src/RedactoPrivacyCenter/lib/api.ts +70 -28
  30. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +4 -2
  31. package/src/RedactoPrivacyCenter/lib/types.ts +79 -36
  32. package/src/RedactoPrivacyCenter/locales/as/translation.json +12 -1
  33. package/src/RedactoPrivacyCenter/locales/bho/translation.json +12 -1
  34. package/src/RedactoPrivacyCenter/locales/bn/translation.json +12 -1
  35. package/src/RedactoPrivacyCenter/locales/brx/translation.json +12 -1
  36. package/src/RedactoPrivacyCenter/locales/doi/translation.json +12 -1
  37. package/src/RedactoPrivacyCenter/locales/en/translation.json +16 -1
  38. package/src/RedactoPrivacyCenter/locales/gom/translation.json +12 -1
  39. package/src/RedactoPrivacyCenter/locales/gu/translation.json +12 -1
  40. package/src/RedactoPrivacyCenter/locales/hi/translation.json +15 -1
  41. package/src/RedactoPrivacyCenter/locales/kn/translation.json +12 -1
  42. package/src/RedactoPrivacyCenter/locales/ks/translation.json +12 -1
  43. package/src/RedactoPrivacyCenter/locales/mai/translation.json +12 -1
  44. package/src/RedactoPrivacyCenter/locales/ml/translation.json +12 -1
  45. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +12 -1
  46. package/src/RedactoPrivacyCenter/locales/mr/translation.json +12 -1
  47. package/src/RedactoPrivacyCenter/locales/ne/translation.json +12 -1
  48. package/src/RedactoPrivacyCenter/locales/or/translation.json +12 -1
  49. package/src/RedactoPrivacyCenter/locales/pa/translation.json +12 -1
  50. package/src/RedactoPrivacyCenter/locales/sa/translation.json +12 -1
  51. package/src/RedactoPrivacyCenter/locales/sat/translation.json +12 -1
  52. package/src/RedactoPrivacyCenter/locales/sd/translation.json +12 -1
  53. package/src/RedactoPrivacyCenter/locales/ta/translation.json +12 -1
  54. package/src/RedactoPrivacyCenter/locales/te/translation.json +12 -1
  55. package/src/RedactoPrivacyCenter/locales/ur/translation.json +12 -1
  56. package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +66 -0
  57. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +528 -114
  58. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +3 -2
  59. package/src/RedactoPrivacyCenter/styles/variables.css +5 -42
  60. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +36 -16
  61. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +4 -12
  62. package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -10
  63. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +1 -1
  64. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +51 -5
  65. package/src/privacy-center.ts +7 -3
  66. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +0 -85
  67. package/src/RedactoPrivacyCenter/context/theme.tsx +0 -127
  68. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +0 -72
@@ -1,8 +1,7 @@
1
1
  /* CSS Custom Properties for Privacy Center SDK */
2
2
  /* These variables are scoped and won't conflict with host app styles */
3
3
 
4
- :root,
5
- [data-theme="light"] {
4
+ :root {
6
5
  /* Colors */
7
6
  --pc-primary: #3b82f6;
8
7
  --pc-primary-hover: #2563eb;
@@ -89,45 +88,9 @@
89
88
 
90
89
  /* Icon filters for dark theme */
91
90
  --pc-icon-filter: 0;
92
- }
93
-
94
- [data-theme="dark"] {
95
- /* Colors */
96
- --pc-primary: #60a5fa;
97
- --pc-primary-hover: #3b82f6;
98
- --pc-primary-active: #2563eb;
99
- --pc-secondary: #9ca3af;
100
- --pc-secondary-hover: #6b7280;
101
- --pc-success: #34d399;
102
- --pc-error: #f87171;
103
- --pc-warning: #fbbf24;
104
- --pc-warning-bg: #78350f;
105
- --pc-warning-border: #fbbf24;
106
- --pc-warning-text: #fef3c7;
107
- --pc-info: #60a5fa;
108
91
 
109
- /* Backgrounds */
110
- --pc-bg-shell: #111827;
111
- --pc-bg-primary: #1b2435;
112
- --pc-bg-secondary: #243044;
113
- --pc-bg-tertiary: #31405a;
114
- --pc-bg-hover: #243044;
115
- --pc-bg-active: #31405a;
116
- --pc-bg-overlay: rgba(0, 0, 0, 0.7);
117
-
118
- /* Text */
119
- --pc-text-primary: #f9fafb;
120
- --pc-text-secondary: #d1d5db;
121
- --pc-text-tertiary: #9ca3af;
122
- --pc-text-inverse: #111827;
123
- --pc-text-disabled: #6b7280;
124
-
125
- /* Borders */
126
- --pc-border-color: rgba(148, 163, 184, 0.18);
127
- --pc-border-color-hover: rgba(148, 163, 184, 0.28);
128
- --pc-border-color-focus: #60a5fa;
129
- --pc-sidebar-shadow: 14px 0 36px rgba(2, 6, 23, 0.42), 1px 0 0 rgba(148, 163, 184, 0.08);
130
-
131
- /* Icon filters for dark theme */
132
- --pc-icon-filter: 1;
92
+ /* Manage Consent toolbar: search, filters, export */
93
+ --pc-consent-toolbar-control-height: 2rem;
94
+ /* Header: language, notifications, theme (same pixel row as compact controls) */
95
+ --pc-navbar-actions-control-height: var(--pc-consent-toolbar-control-height);
133
96
  }
@@ -67,6 +67,7 @@ export default function AdvancedTable<TData extends Record<string, any>>({
67
67
  expandedComponent,
68
68
  }: AdvancedTableProps<TData>) {
69
69
  const { t } = useTranslation();
70
+ const [hoveredRowId, setHoveredRowId] = React.useState<string | null>(null);
70
71
  if (!table) return null;
71
72
 
72
73
  return (
@@ -111,8 +112,7 @@ export default function AdvancedTable<TData extends Record<string, any>>({
111
112
  const onRowClick = meta.onRowClick;
112
113
  const handleRowClick = onRowClick
113
114
  ? (e: React.MouseEvent) => {
114
- e.preventDefault();
115
- onRowClick(row.original);
115
+ onRowClick(row.original, row);
116
116
  }
117
117
  : undefined;
118
118
 
@@ -124,8 +124,23 @@ export default function AdvancedTable<TData extends Record<string, any>>({
124
124
  <React.Fragment key={row.id}>
125
125
  <PCTable.Row
126
126
  className={classNames?.rowClassName}
127
- style={onRowClick ? { cursor: 'pointer' } : {}}
127
+ style={{
128
+ ...(onRowClick ? { cursor: 'pointer' } : {}),
129
+ ...(row.getIsExpanded() ? { borderBottom: 'none' } : {}),
130
+ ...(hoveredRowId === row.id
131
+ ? { backgroundColor: getVar('--pc-bg-hover') }
132
+ : {}),
133
+ }}
128
134
  onClick={handleRowClick}
135
+ onMouseEnter={() => setHoveredRowId(row.id)}
136
+ onMouseLeave={(e) => {
137
+ const nextSibling = e.currentTarget.nextElementSibling;
138
+ const relatedTarget = e.relatedTarget as Node | null;
139
+ if (nextSibling && relatedTarget && nextSibling.contains(relatedTarget)) {
140
+ return;
141
+ }
142
+ setHoveredRowId((prev) => (prev === row.id ? null : prev));
143
+ }}
129
144
  >
130
145
  {row.getVisibleCells().map((cell) => (
131
146
  <TableCellBasic
@@ -141,12 +156,16 @@ export default function AdvancedTable<TData extends Record<string, any>>({
141
156
  <PCTable.Row
142
157
  className={classNames?.expandedRowClassName}
143
158
  style={{
144
- overflow: 'hidden',
145
- transition: 'all 0.3s ease-in-out',
146
- maxHeight: row.getIsExpanded() ? '2000px' : '0',
147
- opacity: row.getIsExpanded() ? 1 : 0,
148
- height: row.getIsExpanded() ? 'auto' : '0',
149
- minHeight: row.getIsExpanded() ? 'auto' : '0',
159
+ pointerEvents: row.getIsExpanded() ? 'auto' : 'none',
160
+ }}
161
+ onMouseEnter={() => setHoveredRowId(row.id)}
162
+ onMouseLeave={(e) => {
163
+ const previousSibling = e.currentTarget.previousElementSibling;
164
+ const relatedTarget = e.relatedTarget as Node | null;
165
+ if (previousSibling && relatedTarget && previousSibling.contains(relatedTarget)) {
166
+ return;
167
+ }
168
+ setHoveredRowId((prev) => (prev === row.id ? null : prev));
150
169
  }}
151
170
  >
152
171
  <PCTable.Cell
@@ -154,18 +173,19 @@ export default function AdvancedTable<TData extends Record<string, any>>({
154
173
  className={classNames?.expandedCellClassName}
155
174
  style={{
156
175
  padding: row.getIsExpanded() ? undefined : '0',
157
- transition: 'all 0.3s ease-in-out',
158
- height: row.getIsExpanded() ? 'auto' : '0',
159
- paddingTop: row.getIsExpanded() ? undefined : '0',
160
- paddingBottom: row.getIsExpanded() ? undefined : '0',
176
+ height: row.getIsExpanded() ? undefined : '0',
177
+ border: 'none',
161
178
  }}
162
179
  >
163
180
  <div
164
181
  style={{
165
- transition: 'all 0.3s ease-in-out',
166
- transform: row.getIsExpanded() ? 'translateY(0)' : 'translateY(-8px)',
182
+ overflow: 'hidden',
183
+ maxHeight: row.getIsExpanded() ? '1200px' : '0',
167
184
  opacity: row.getIsExpanded() ? 1 : 0,
168
- height: row.getIsExpanded() ? 'auto' : '0',
185
+ transform: row.getIsExpanded() ? 'translateY(0)' : 'translateY(-6px)',
186
+ transition:
187
+ 'max-height 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease, transform 220ms ease',
188
+ willChange: 'max-height, opacity, transform',
169
189
  }}
170
190
  >
171
191
  {rowExpandedComponent(row)}
@@ -58,10 +58,10 @@ export default function AdvancedTablePagination<
58
58
  ...pcStyles.flexCol,
59
59
  ...pcStyles.itemsCenter,
60
60
  width: '100%',
61
- paddingTop: getVar('--pc-spacing-sm'),
62
- paddingBottom: getVar('--pc-spacing-sm'),
63
- gap: getVar('--pc-spacing-sm'),
64
- minHeight: '52px',
61
+ paddingTop: '0.375rem',
62
+ paddingBottom: '0.125rem',
63
+ gap: '0.375rem',
64
+ minHeight: '44px',
65
65
  opacity: shouldShowPagination ? 1 : 0.5,
66
66
  pointerEvents: shouldShowPagination ? 'auto' : 'none',
67
67
  }}
@@ -148,7 +148,6 @@ export default function AdvancedTablePagination<
148
148
  disabled={!table.getCanPreviousPage()}
149
149
  style={{
150
150
  borderRadius: '0.5rem',
151
- boxShadow: getVar('--pc-shadow-sm'),
152
151
  borderColor: getVar('--pc-border-color'),
153
152
  color: getVar('--pc-text-primary'),
154
153
  backgroundColor: getVar('--pc-bg-primary'),
@@ -176,7 +175,6 @@ export default function AdvancedTablePagination<
176
175
  disabled={!table.getCanPreviousPage()}
177
176
  style={{
178
177
  borderRadius: '0.5rem',
179
- boxShadow: getVar('--pc-shadow-sm'),
180
178
  borderColor: getVar('--pc-border-color'),
181
179
  color: getVar('--pc-text-primary'),
182
180
  backgroundColor: getVar('--pc-bg-primary'),
@@ -204,7 +202,6 @@ export default function AdvancedTablePagination<
204
202
  disabled={!table.getCanNextPage()}
205
203
  style={{
206
204
  borderRadius: '0.5rem',
207
- boxShadow: getVar('--pc-shadow-sm'),
208
205
  borderColor: getVar('--pc-border-color'),
209
206
  color: getVar('--pc-text-primary'),
210
207
  backgroundColor: getVar('--pc-bg-primary'),
@@ -232,7 +229,6 @@ export default function AdvancedTablePagination<
232
229
  disabled={!table.getCanNextPage()}
233
230
  style={{
234
231
  borderRadius: '0.5rem',
235
- boxShadow: getVar('--pc-shadow-sm'),
236
232
  borderColor: getVar('--pc-border-color'),
237
233
  color: getVar('--pc-text-primary'),
238
234
  backgroundColor: getVar('--pc-bg-primary'),
@@ -364,7 +360,6 @@ export default function AdvancedTablePagination<
364
360
  disabled={!table.getCanPreviousPage()}
365
361
  style={{
366
362
  borderRadius: '0.5rem',
367
- boxShadow: getVar('--pc-shadow-sm'),
368
363
  borderColor: getVar('--pc-border-color'),
369
364
  color: getVar('--pc-text-primary'),
370
365
  backgroundColor: getVar('--pc-bg-primary'),
@@ -392,7 +387,6 @@ export default function AdvancedTablePagination<
392
387
  disabled={!table.getCanPreviousPage()}
393
388
  style={{
394
389
  borderRadius: '0.5rem',
395
- boxShadow: getVar('--pc-shadow-sm'),
396
390
  borderColor: getVar('--pc-border-color'),
397
391
  color: getVar('--pc-text-primary'),
398
392
  backgroundColor: getVar('--pc-bg-primary'),
@@ -420,7 +414,6 @@ export default function AdvancedTablePagination<
420
414
  disabled={!table.getCanNextPage()}
421
415
  style={{
422
416
  borderRadius: '0.5rem',
423
- boxShadow: getVar('--pc-shadow-sm'),
424
417
  borderColor: getVar('--pc-border-color'),
425
418
  color: getVar('--pc-text-primary'),
426
419
  backgroundColor: getVar('--pc-bg-primary'),
@@ -448,7 +441,6 @@ export default function AdvancedTablePagination<
448
441
  disabled={!table.getCanNextPage()}
449
442
  style={{
450
443
  borderRadius: '0.5rem',
451
- boxShadow: getVar('--pc-shadow-sm'),
452
444
  borderColor: getVar('--pc-border-color'),
453
445
  color: getVar('--pc-text-primary'),
454
446
  backgroundColor: getVar('--pc-bg-primary'),
@@ -1,13 +1,15 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
  import { pcStyles } from '../styles/pcStyles';
3
3
  import PoweredByRedacto from '../assets/poweredByRedacto.svg';
4
- import { useTheme } from '../context/theme';
4
+ import { useBranding } from '../context/BrandingContext';
5
5
 
6
6
  const getVar = (varName: string): string => `var(${varName})`;
7
7
 
8
8
  export default function Footer() {
9
9
  const { t } = useTranslation();
10
- const { theme } = useTheme();
10
+ const branding = useBranding();
11
+
12
+ if (branding?.hide_platform_branding) return null;
11
13
 
12
14
  return (
13
15
  <footer
@@ -46,15 +48,7 @@ export default function Footer() {
46
48
  alignItems: 'center',
47
49
  justifyContent: 'center',
48
50
  minHeight: '1.75rem',
49
- padding:
50
- theme === 'dark' ? '0.25rem 0.5rem' : 0,
51
- backgroundColor:
52
- theme === 'dark' ? 'rgba(255, 255, 255, 0.92)' : 'transparent',
53
51
  borderRadius: '9999px',
54
- boxShadow:
55
- theme === 'dark'
56
- ? '0 8px 18px rgba(2, 6, 23, 0.28)'
57
- : 'none',
58
52
  }}
59
53
  >
60
54
  <img
@@ -75,7 +75,7 @@ export function PCInput({
75
75
  return (
76
76
  <div className={className} style={wrapperStyle}>
77
77
  {label && <label style={pcStyles.inputLabel}>{label}</label>}
78
- <div style={pcStyles.inputWithPrefix}>
78
+ <div className="pc-input-affix" style={pcStyles.inputWithPrefix}>
79
79
  {prefix && <span style={pcStyles.inputPrefix}>{prefix}</span>}
80
80
  <input
81
81
  ref={inputRef}
@@ -7,7 +7,7 @@ import {
7
7
  useLayoutEffect,
8
8
  } from 'react';
9
9
  import { pcStyles } from '../styles/pcStyles';
10
- import { ChevronDown, X } from 'lucide-react';
10
+ import { ChevronDown, X, Check } from 'lucide-react';
11
11
 
12
12
  const getVar = (varName: string): string => `var(${varName})`;
13
13
  const useIsomorphicLayoutEffect =
@@ -32,6 +32,7 @@ export interface PCSelectProps {
32
32
  disabled?: boolean;
33
33
  size?: 'sm' | 'md' | 'lg';
34
34
  label?: ReactNode;
35
+ prefix?: ReactNode;
35
36
  /**
36
37
  * If true, the dropdown list width will match the trigger width.
37
38
  * Useful in tight filter rows where you want trigger + dropdown aligned.
@@ -40,6 +41,7 @@ export interface PCSelectProps {
40
41
  searchPlaceholder?: string;
41
42
  noOptionsMessage?: string;
42
43
  clearAriaLabel?: string;
44
+ showSelectedCheck?: boolean;
43
45
  }
44
46
 
45
47
  export function PCSelect({
@@ -55,10 +57,12 @@ export function PCSelect({
55
57
  disabled = false,
56
58
  size = 'md',
57
59
  label,
60
+ prefix,
58
61
  dropdownMatchTriggerWidth = false,
59
62
  searchPlaceholder = 'Search...',
60
63
  noOptionsMessage = 'No options found',
61
64
  clearAriaLabel = 'Clear selection',
65
+ showSelectedCheck = false,
62
66
  }: PCSelectProps) {
63
67
  const [isOpen, setIsOpen] = useState(false);
64
68
  const [searchQuery, setSearchQuery] = useState('');
@@ -112,7 +116,7 @@ export function PCSelect({
112
116
  const paddingLeft = parseFloat(computed.paddingLeft || '0') || 0;
113
117
  const paddingRight = parseFloat(computed.paddingRight || '0') || 0;
114
118
  // Chevron + spacing (and optional clear button) lives on the right
115
- const iconAreaPx = clearable ? 52 : 32;
119
+ const iconAreaPx = clearable ? 52 : showSelectedCheck ? 46 : 32;
116
120
 
117
121
  const maxLabelWidth = options.reduce((max, opt) => {
118
122
  const w = measureTextWidth(opt.label ?? '', font);
@@ -267,6 +271,17 @@ export function PCSelect({
267
271
  setIsOpen((prev) => !prev);
268
272
  }}
269
273
  >
274
+ {prefix && (
275
+ <span style={{
276
+ display: 'inline-flex',
277
+ alignItems: 'center',
278
+ flexShrink: 0,
279
+ marginRight: '0.375rem',
280
+ color: pcStyles.textSecondary.color,
281
+ }}>
282
+ {prefix}
283
+ </span>
284
+ )}
270
285
  <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
271
286
  {value ? value.label : <span style={{ color: pcStyles.textSecondary.color }}>{placeholder}</span>}
272
287
  </span>
@@ -349,10 +364,14 @@ export function PCSelect({
349
364
  <div
350
365
  key={String(option.value)}
351
366
  className="pc-select-option"
367
+ data-selected={value?.value === option.value ? 'true' : 'false'}
352
368
  style={{
353
369
  ...optionStyle,
354
370
  backgroundColor: getVar('--pc-bg-primary'),
355
- color: getVar('--pc-text-primary'),
371
+ color:
372
+ value?.value === option.value && showSelectedCheck
373
+ ? getVar('--pc-primary')
374
+ : getVar('--pc-text-primary'),
356
375
  transition: 'background-color 0.2s ease, color 0.2s ease',
357
376
  whiteSpace: 'nowrap',
358
377
  overflow: 'hidden',
@@ -360,7 +379,9 @@ export function PCSelect({
360
379
  maxWidth: '100%',
361
380
  ...(option.disabled && { opacity: 0.5, cursor: 'not-allowed' }),
362
381
  ...(value?.value === option.value && {
363
- backgroundColor: getVar('--pc-bg-hover'),
382
+ backgroundColor: showSelectedCheck
383
+ ? 'rgba(59, 130, 246, 0.12)'
384
+ : getVar('--pc-bg-hover'),
364
385
  }),
365
386
  }}
366
387
  onClick={() => handleSelect(option)}
@@ -375,7 +396,32 @@ export function PCSelect({
375
396
  : getVar('--pc-bg-primary');
376
397
  }}
377
398
  >
378
- {option.label}
399
+ <span
400
+ style={{
401
+ display: 'flex',
402
+ alignItems: 'center',
403
+ justifyContent: 'space-between',
404
+ gap: '0.5rem',
405
+ width: '100%',
406
+ minWidth: 0,
407
+ }}
408
+ >
409
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
410
+ {option.label}
411
+ </span>
412
+ {showSelectedCheck && (
413
+ <span
414
+ style={{
415
+ display: 'inline-flex',
416
+ alignItems: 'center',
417
+ opacity: value?.value === option.value ? 1 : 0,
418
+ flexShrink: 0,
419
+ }}
420
+ >
421
+ <Check size={14} strokeWidth={2.5} />
422
+ </span>
423
+ )}
424
+ </span>
379
425
  </div>
380
426
  ))
381
427
  )}
@@ -37,11 +37,14 @@ export type {
37
37
  TokenExchangeResponse,
38
38
  FetchOrgParams,
39
39
  ConsentDataElement,
40
- UserConsent,
40
+ PurposeItem,
41
+ ConsentGroup,
42
+ DisplayPurposeRow,
43
+ ConsentModalRecord,
44
+ GroupPurposesOut,
45
+ PaginationMeta,
41
46
  NominatorInfo,
42
47
  ConsentHistoryStatusSummary,
43
- ProductConsentHistoryGroup,
44
- NominatedPurposesGroup,
45
48
  Pagination,
46
49
  UserConsentDetail,
47
50
  UserConsentResponse,
@@ -70,6 +73,7 @@ export {
70
73
  createCaseRequest,
71
74
  fetchProducts,
72
75
  fetchUserConsents,
76
+ fetchGroupPurposes,
73
77
  manageConsent,
74
78
  fetchActivities,
75
79
  fetchCaseHistory,
@@ -1,85 +0,0 @@
1
- import { useEffect, useRef } from 'react';
2
- import { IoSunnyOutline, IoMoonOutline } from 'react-icons/io5';
3
- import { useTheme } from '../context/theme';
4
-
5
- export default function ThemeTransitionOverlay() {
6
- const { isTransitioning, targetTheme } = useTheme();
7
- const overlayRef = useRef<HTMLDivElement>(null);
8
- const iconRef = useRef<HTMLDivElement>(null);
9
-
10
- useEffect(() => {
11
- if (isTransitioning && overlayRef.current && iconRef.current) {
12
- const overlay = overlayRef.current;
13
- const icon = iconRef.current;
14
-
15
- // Trigger animation
16
- overlay.style.display = 'flex';
17
- // Force reflow to ensure display change is applied
18
- overlay.offsetHeight;
19
- overlay.style.opacity = '1';
20
- icon.style.animation = 'none';
21
- // Force reflow
22
- icon.offsetHeight;
23
- icon.style.animation = '';
24
-
25
- const timeout = setTimeout(() => {
26
- if (overlay) {
27
- overlay.style.opacity = '0';
28
- setTimeout(() => {
29
- if (overlay) {
30
- overlay.style.display = 'none';
31
- }
32
- }, 300);
33
- }
34
- }, 1000);
35
-
36
- return () => clearTimeout(timeout);
37
- }
38
- }, [isTransitioning, targetTheme]);
39
-
40
- if (!targetTheme) return null;
41
-
42
- const Icon = targetTheme === 'light' ? IoSunnyOutline : IoMoonOutline;
43
- // Sun: golden yellow, Moon: soft blue/white
44
- const iconColor = targetTheme === 'light' ? '#fbbf24' : '#c7d2fe';
45
-
46
- return (
47
- <div
48
- ref={overlayRef}
49
- className="pc-theme-transition-overlay"
50
- style={{
51
- display: 'none',
52
- opacity: 0,
53
- position: 'fixed',
54
- top: 0,
55
- left: 0,
56
- right: 0,
57
- bottom: 0,
58
- pointerEvents: 'none',
59
- zIndex: 9999,
60
- alignItems: 'center',
61
- justifyContent: 'center',
62
- transition: 'opacity 0.3s ease',
63
- }}
64
- aria-hidden="true"
65
- >
66
- <div
67
- ref={iconRef}
68
- className={`pc-theme-transition-icon pc-theme-transition-${targetTheme}`}
69
- style={{
70
- position: 'absolute',
71
- bottom: '-150px',
72
- left: '50%',
73
- transform: 'translateX(-50%)',
74
- fontSize: '120px',
75
- color: iconColor,
76
- display: 'flex',
77
- alignItems: 'center',
78
- justifyContent: 'center',
79
- }}
80
- >
81
- <Icon size={120} />
82
- </div>
83
- </div>
84
- );
85
- }
@@ -1,127 +0,0 @@
1
- import {
2
- createContext,
3
- useContext,
4
- useState,
5
- useEffect,
6
- useCallback,
7
- type ReactNode,
8
- } from 'react';
9
-
10
- export type Theme = 'light' | 'dark';
11
-
12
- type ThemeContextType = {
13
- theme: Theme;
14
- toggleTheme: () => void;
15
- setTheme: (theme: Theme) => void;
16
- isTransitioning: boolean;
17
- targetTheme: Theme | null;
18
- };
19
-
20
- const THEME_STORAGE_KEY = 'privacy-center-theme';
21
-
22
- const ThemeContext = createContext<ThemeContextType | null>(null);
23
-
24
- function getInitialTheme(defaultTheme?: Theme): Theme {
25
- // If defaultTheme is provided, always use it (takes precedence over sessionStorage)
26
- // This ensures that when a parent component explicitly sets a theme, it's respected
27
- if (defaultTheme !== undefined) {
28
- return defaultTheme;
29
- }
30
-
31
- // Otherwise, check sessionStorage first
32
- if (typeof window !== 'undefined') {
33
- const stored = sessionStorage.getItem(THEME_STORAGE_KEY);
34
- if (stored === 'light' || stored === 'dark') {
35
- return stored;
36
- }
37
-
38
- // Fallback to system preference
39
- if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
40
- return 'dark';
41
- }
42
- }
43
-
44
- return 'light';
45
- }
46
-
47
- type ThemeProviderProps = {
48
- children: ReactNode;
49
- defaultTheme?: Theme;
50
- };
51
-
52
- export const ThemeProvider = ({ children, defaultTheme }: ThemeProviderProps) => {
53
- // Initialize with defaultTheme if provided, otherwise use stored/system preference
54
- // When defaultTheme is provided, it takes precedence over sessionStorage
55
- const [theme, setThemeState] = useState<Theme>(() => getInitialTheme(defaultTheme));
56
- const [isTransitioning, setIsTransitioning] = useState(false);
57
- const [targetTheme, setTargetTheme] = useState<Theme | null>(null);
58
-
59
- // Ensure defaultTheme is applied on mount if provided
60
- // This handles the case where sessionStorage had a different value
61
- useEffect(() => {
62
- if (defaultTheme !== undefined) {
63
- setThemeState(defaultTheme);
64
- }
65
- }, []); // Only run on mount
66
-
67
- const setTheme = useCallback((newTheme: Theme) => {
68
- if (newTheme !== theme) {
69
- setIsTransitioning(true);
70
- setTargetTheme(newTheme);
71
- setThemeState(newTheme);
72
- // Reset transition state after animation completes
73
- setTimeout(() => {
74
- setIsTransitioning(false);
75
- setTargetTheme(null);
76
- }, 1000);
77
- }
78
- }, [theme]);
79
-
80
- const toggleTheme = useCallback(() => {
81
- setIsTransitioning(true);
82
- const newTheme = theme === 'light' ? 'dark' : 'light';
83
- setTargetTheme(newTheme);
84
- setThemeState(newTheme);
85
- // Reset transition state after animation completes
86
- setTimeout(() => {
87
- setIsTransitioning(false);
88
- setTargetTheme(null);
89
- }, 1000);
90
- }, [theme]);
91
-
92
- // Apply theme to document and persist to sessionStorage
93
- useEffect(() => {
94
- document.documentElement.setAttribute('data-theme', theme);
95
- sessionStorage.setItem(THEME_STORAGE_KEY, theme);
96
- }, [theme]);
97
-
98
- // Listen for system theme changes
99
- useEffect(() => {
100
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
101
-
102
- const handleChange = (e: MediaQueryListEvent) => {
103
- // Only auto-switch if user hasn't explicitly set a preference
104
- const stored = sessionStorage.getItem(THEME_STORAGE_KEY);
105
- if (!stored) {
106
- setThemeState(e.matches ? 'dark' : 'light');
107
- }
108
- };
109
-
110
- mediaQuery.addEventListener('change', handleChange);
111
- return () => mediaQuery.removeEventListener('change', handleChange);
112
- }, []);
113
-
114
- return (
115
- <ThemeContext.Provider value={{ theme, toggleTheme, setTheme, isTransitioning, targetTheme }}>
116
- {children}
117
- </ThemeContext.Provider>
118
- );
119
- };
120
-
121
- export const useTheme = (): ThemeContextType => {
122
- const context = useContext(ThemeContext);
123
- if (!context) {
124
- throw new Error('useTheme must be used within a ThemeProvider');
125
- }
126
- return context;
127
- };