@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
@@ -0,0 +1,102 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { PCEmpty, PCText } from '../ui';
3
+ import type { ConsentModalRecord, DisplayPurposeRow } from '../lib/types';
4
+ import {
5
+ renderGivenDate,
6
+ renderMethod,
7
+ renderModifyButton,
8
+ renderStatusBadge,
9
+ renderValidTill,
10
+ } from './ConsentManagerCells';
11
+
12
+ type ConsentManagerPurposeCardsProps = {
13
+ rows: DisplayPurposeRow[];
14
+ onModify: (record: ConsentModalRecord) => void;
15
+ };
16
+
17
+ const toModalRecord = (row: DisplayPurposeRow): ConsentModalRecord => ({
18
+ ...row,
19
+ });
20
+
21
+ export function ConsentManagerPurposeCards({
22
+ rows,
23
+ onModify,
24
+ }: ConsentManagerPurposeCardsProps) {
25
+ const { t } = useTranslation();
26
+
27
+ if (rows.length === 0) {
28
+ return (
29
+ <div className="pc-consent-mobile-empty">
30
+ <PCEmpty title={t('noData')} />
31
+ </div>
32
+ );
33
+ }
34
+
35
+ return (
36
+ <div className="pc-consent-mobile-list">
37
+ {rows.map((row) => (
38
+ <article
39
+ key={`${row.product_uuid}:${row.purpose_uuid}`}
40
+ className="pc-consent-mobile-card"
41
+ >
42
+ <div className="pc-consent-mobile-purpose-head">
43
+ <div style={{ minWidth: 0 }}>
44
+ <PCText size="sm" weight="semibold">{row.name}</PCText>
45
+ {row.description && (
46
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-subtle-text">
47
+ {row.description}
48
+ </PCText>
49
+ )}
50
+ </div>
51
+ {renderStatusBadge(row.status, t)}
52
+ </div>
53
+
54
+ <div className="pc-consent-mobile-activity-row">
55
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-meta-label">
56
+ {t('product')}
57
+ </PCText>
58
+ <PCText size="sm" weight="medium">{row.product_name || '-'}</PCText>
59
+ </div>
60
+
61
+ {row.nominator && (
62
+ <div className="pc-consent-mobile-activity-row">
63
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-meta-label">
64
+ {t('nominatedBy')}
65
+ </PCText>
66
+ <PCText size="sm" weight="medium">
67
+ {row.nominator.name || row.nominator.email || row.nominator.org_user_id}
68
+ </PCText>
69
+ </div>
70
+ )}
71
+
72
+ <div className="pc-consent-mobile-meta-grid">
73
+ <div>
74
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-meta-label">
75
+ {t('givenDate')}
76
+ </PCText>
77
+ {renderGivenDate(row.given_date, { showTooltip: false })}
78
+ </div>
79
+ <div>
80
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-meta-label">
81
+ {t('validTill')}
82
+ </PCText>
83
+ {renderValidTill(row, t)}
84
+ </div>
85
+ <div>
86
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-meta-label">
87
+ {t('method')}
88
+ </PCText>
89
+ {renderMethod(row.method)}
90
+ </div>
91
+ <div className="pc-consent-mobile-action-cell">
92
+ <PCText size="sm" color="secondary" className="pc-consent-mobile-meta-label">
93
+ {t('action')}
94
+ </PCText>
95
+ {renderModifyButton(toModalRecord(row), onModify, t)}
96
+ </div>
97
+ </div>
98
+ </article>
99
+ ))}
100
+ </div>
101
+ );
102
+ }
@@ -6,8 +6,9 @@ const getVar = (varName: string): string => `var(${varName})`;
6
6
  export const ConsentManagerTableSkeleton = () => {
7
7
  return (
8
8
  <div style={{
9
- borderRadius: getVar('--pc-border-radius-md'),
9
+ borderRadius: `${getVar('--pc-border-radius-lg')} ${getVar('--pc-border-radius-lg')} 0 0`,
10
10
  border: `1px solid ${getVar('--pc-border-color')}`,
11
+ borderBottom: 'none',
11
12
  backgroundColor: getVar('--pc-bg-primary'),
12
13
  }}>
13
14
  {/* Table Header */}
@@ -44,7 +45,10 @@ export const ConsentManagerTableSkeleton = () => {
44
45
  <div
45
46
  key={index}
46
47
  style={{
47
- borderBottom: `1px solid ${getVar('--pc-border-color')}`,
48
+ borderBottom:
49
+ index === SKELETON_ROW_COUNT - 1
50
+ ? 'none'
51
+ : `1px solid ${getVar('--pc-border-color')}`,
48
52
  backgroundColor: getVar('--pc-bg-primary'),
49
53
  }}
50
54
  >
@@ -1483,11 +1483,11 @@ export default function Form({
1483
1483
  className="pc-flex-responsive pc-spacing-responsive"
1484
1484
  >
1485
1485
  {isPopup ? (
1486
- <PCButton variant="outline" onClick={onClose} style={{ flexShrink: 0 }}>
1486
+ <PCButton variant="secondary" onClick={onClose} style={{ flexShrink: 0 }}>
1487
1487
  {t('cancel') ?? 'Cancel'}
1488
1488
  </PCButton>
1489
1489
  ) : (
1490
- <PCButton variant="outline" style={{ flexShrink: 0 }}>
1490
+ <PCButton variant="secondary" style={{ flexShrink: 0 }}>
1491
1491
  {t('saveDraft')}
1492
1492
  </PCButton>
1493
1493
  )}
@@ -1523,7 +1523,7 @@ export default function Form({
1523
1523
  footer={
1524
1524
  <div className="pc-erasure-modal-footer">
1525
1525
  <PCButton
1526
- variant="outline"
1526
+ variant="secondary"
1527
1527
  onClick={() => handleErasureModalChoice(false)}
1528
1528
  disabled={isLoading}
1529
1529
  >
@@ -19,7 +19,7 @@ export const ModifyConsentModal = ({
19
19
  const [isAccordionOpen, setIsAccordionOpen] = useState(false);
20
20
  const [selectedDataElementUuids, setSelectedDataElementUuids] = useState<string[]>([]);
21
21
 
22
- const nominatorInfo = selectedRecord?.nominator_info;
22
+ const nominatorInfo = selectedRecord?.nominator;
23
23
  const nominatorLabel = nominatorInfo?.name || nominatorInfo?.email || nominatorInfo?.org_user_id || null;
24
24
 
25
25
  const actionConfig = useMemo(() => {
@@ -39,7 +39,10 @@ export const ModifyConsentModal = ({
39
39
  };
40
40
  }
41
41
 
42
- if (status === ConsentStatusEnum.WITHDRAWN) {
42
+ if (
43
+ status === ConsentStatusEnum.WITHDRAWN ||
44
+ status === ConsentStatusEnum.REVOKED
45
+ ) {
43
46
  return {
44
47
  action: 'regrant' as ConsentAction,
45
48
  buttonLabel: t('regrantConsent'),
@@ -158,7 +161,7 @@ export const ModifyConsentModal = ({
158
161
  fontWeight: pcStyles.fontWeightSemibold.fontWeight,
159
162
  color: pcStyles.textPrimary.color,
160
163
  }}>
161
- {selectedRecord.purpose}
164
+ {selectedRecord.name}
162
165
  </p>
163
166
  <p style={{
164
167
  textAlign: 'left',
@@ -167,7 +170,7 @@ export const ModifyConsentModal = ({
167
170
  color: pcStyles.textSecondary.color,
168
171
  margin: 0,
169
172
  }}>
170
- {selectedRecord.purpose_description}
173
+ {selectedRecord.description}
171
174
  </p>
172
175
  </div>
173
176
  <ChevronDownIcon
@@ -297,7 +300,7 @@ export const ModifyConsentModal = ({
297
300
  ...pcStyles.gapSm,
298
301
  ...pcStyles.justifyEnd,
299
302
  }}>
300
- <PCButton variant="outline" onClick={onClose}>
303
+ <PCButton variant="secondary" onClick={onClose}>
301
304
  {t('cancel')}
302
305
  </PCButton>
303
306
  <PCButton
@@ -309,7 +312,7 @@ export const ModifyConsentModal = ({
309
312
  variant={actionConfig.buttonVariant}
310
313
  onClick={() => {
311
314
  if (selectedRecord.purpose_uuid) {
312
- const nomContact = selectedRecord.nominator_info?.email || selectedRecord.nominator_info?.org_user_id || undefined;
315
+ const nomContact = selectedRecord.nominator?.email || selectedRecord.nominator?.org_user_id || undefined;
313
316
  onManageConsent(
314
317
  selectedRecord.purpose_uuid,
315
318
  actionConfig.action,
@@ -148,7 +148,7 @@ export const NotificationBell = () => {
148
148
  <PCTooltip content={t('notifications')} placement="bottom" color="invert">
149
149
  <div className="pc-notification-bell-wrapper">
150
150
  <button type="button" aria-label={t('notifications')} className="pc-notification-bell-btn">
151
- <IoNotificationsOutline size={20} />
151
+ <IoNotificationsOutline size={18} />
152
152
  </button>
153
153
  {unreadCount > 0 && (
154
154
  <span className="pc-notification-badge">{unreadCount}</span>
@@ -1,8 +1,6 @@
1
1
  import { Suspense, useState, useEffect, useCallback } from 'react';
2
2
  import SideBarPC from './SideMenuBarPC/SideBarPC';
3
3
  import Footer from '../ui/Footer';
4
- import ThemeToggle from '../ui/ThemeToggle';
5
- import ThemeTransitionOverlay from './ThemeTransitionOverlay';
6
4
  import { PCSelect } from '../ui';
7
5
  import { useTranslation } from 'react-i18next';
8
6
  import { PAGE_CHROME_BY_PAGE, languageOptions } from '../lib/constants';
@@ -14,6 +12,7 @@ import { pcStyles } from '../styles/pcStyles';
14
12
  import type { PrivacyCenterLayoutProps } from './types';
15
13
  import { NotificationBell } from './NotificationHub/NotificationBell';
16
14
  import { getVar } from '../lib/utils';
15
+ import { Globe2 } from 'lucide-react';
17
16
 
18
17
  export default function PrivacyCenterLayout({
19
18
  children,
@@ -23,7 +22,7 @@ export default function PrivacyCenterLayout({
23
22
  }: PrivacyCenterLayoutProps) {
24
23
  const { t } = useTranslation();
25
24
  const { currentPage } = useInternalNavigation();
26
- const { pageChromeOverride } = usePageChrome();
25
+ const { pageChromeOverride, headerStatus } = usePageChrome();
27
26
  const [locale, setLocale] = useState(i18n.language);
28
27
  const pageChrome = pageChromeOverride ?? PAGE_CHROME_BY_PAGE[currentPage];
29
28
 
@@ -41,7 +40,6 @@ export default function PrivacyCenterLayout({
41
40
  return (
42
41
  <LocaleContext.Provider value={{ locale, setLocale }}>
43
42
  <Suspense fallback={<div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.justifyCenter, minHeight: '100vh' }}>{t('loadingEllipsis')}</div>}>
44
- <ThemeTransitionOverlay />
45
43
  <main
46
44
  className="pc-root"
47
45
  style={{
@@ -59,12 +57,11 @@ export default function PrivacyCenterLayout({
59
57
  flex: 1,
60
58
  minWidth: 0,
61
59
  backgroundColor: getVar('--pc-bg-shell'),
60
+ minHeight: 0,
62
61
  }} className="pc-content-wrapper">
63
62
  {/* Top Navbar with Theme Toggle */}
64
63
  <nav style={{
65
- position: 'fixed',
66
- top: 0,
67
- zIndex: 50,
64
+ position: 'relative',
68
65
  ...pcStyles.flex,
69
66
  ...pcStyles.itemsCenter,
70
67
  ...pcStyles.justifyBetween,
@@ -74,8 +71,8 @@ export default function PrivacyCenterLayout({
74
71
  paddingRight: 'var(--pc-navbar-padding-right, 0.75rem)',
75
72
  paddingTop: 'var(--pc-navbar-padding-top, 0.625rem)',
76
73
  paddingBottom: 'var(--pc-navbar-padding-bottom, 0.625rem)',
77
- transition: 'background-color 0.3s ease, border-color 0.3s ease, width 0.3s ease, left 0.3s ease',
78
- willChange: 'background-color, border-color, width, left',
74
+ transition: 'background-color 0.3s ease, border-color 0.3s ease',
75
+ willChange: 'background-color, border-color',
79
76
  }} className="pc-navbar">
80
77
  <div
81
78
  className="pc-navbar-page-meta"
@@ -85,32 +82,76 @@ export default function PrivacyCenterLayout({
85
82
  justifyContent: 'center',
86
83
  minWidth: 0,
87
84
  flex: 1,
88
- gap: '0.125rem',
85
+ gap: '0.1875rem',
89
86
  }}
90
87
  >
91
- <p
92
- className="pc-navbar-title"
93
- style={{
94
- margin: 0,
95
- color: getVar('--pc-text-primary'),
96
- fontSize: '1rem',
97
- fontWeight: 600,
98
- lineHeight: 1.3,
99
- whiteSpace: 'nowrap',
100
- overflow: 'hidden',
101
- textOverflow: 'ellipsis',
102
- }}
103
- >
104
- {t(pageChrome.titleKey)}
105
- </p>
88
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', minWidth: 0 }}>
89
+ <p
90
+ className="pc-navbar-title"
91
+ style={{
92
+ margin: 0,
93
+ color: getVar('--pc-text-primary'),
94
+ fontSize: '1.0625rem',
95
+ fontWeight: 700,
96
+ lineHeight: 1.2,
97
+ whiteSpace: 'nowrap',
98
+ overflow: 'hidden',
99
+ textOverflow: 'ellipsis',
100
+ }}
101
+ >
102
+ {t(pageChrome.titleKey)}
103
+ </p>
104
+ {headerStatus && (
105
+ <span
106
+ style={{
107
+ display: 'inline-flex',
108
+ alignItems: 'center',
109
+ flexShrink: 0,
110
+ borderRadius: '9999px',
111
+ gap: '0.375rem',
112
+ padding: '0.25rem 0.75rem',
113
+ fontSize: '0.625rem',
114
+ fontWeight: 600,
115
+ lineHeight: 1.2,
116
+ border: `1px solid ${
117
+ headerStatus.variant === 'success'
118
+ ? 'rgba(16, 185, 129, 0.35)'
119
+ : 'rgba(245, 158, 11, 0.35)'
120
+ }`,
121
+ backgroundColor:
122
+ headerStatus.variant === 'success'
123
+ ? 'rgba(16, 185, 129, 0.1)'
124
+ : '#fff7ed',
125
+ color:
126
+ headerStatus.variant === 'success'
127
+ ? getVar('--pc-success')
128
+ : '#d97706',
129
+ }}
130
+ >
131
+ <span
132
+ aria-hidden="true"
133
+ style={{
134
+ width: '0.4375rem',
135
+ height: '0.4375rem',
136
+ borderRadius: '9999px',
137
+ backgroundColor:
138
+ headerStatus.variant === 'success'
139
+ ? getVar('--pc-success')
140
+ : '#d97706',
141
+ }}
142
+ />
143
+ {headerStatus.label}
144
+ </span>
145
+ )}
146
+ </div>
106
147
  {pageChrome.descriptionKey && (
107
148
  <p
108
149
  className="pc-navbar-description"
109
150
  style={{
110
151
  margin: 0,
111
152
  color: getVar('--pc-text-secondary'),
112
- fontSize: '0.8125rem',
113
- lineHeight: 1.35,
153
+ fontSize: '0.75rem',
154
+ lineHeight: 1.2,
114
155
  whiteSpace: 'nowrap',
115
156
  overflow: 'hidden',
116
157
  textOverflow: 'ellipsis',
@@ -123,18 +164,20 @@ export default function PrivacyCenterLayout({
123
164
  <div style={{
124
165
  display: 'flex',
125
166
  alignItems: 'center',
126
- gap: '0.5rem',
167
+ gap: '0.375rem',
127
168
  flexShrink: 0,
128
169
  }} className="pc-navbar-actions">
129
170
  <PCSelect
171
+ className="pc-navbar-language-select"
130
172
  size="sm"
131
173
  searchable
174
+ prefix={<Globe2 size={12} strokeWidth={1.8} />}
132
175
  searchPlaceholder={t('searchPlaceholder')}
133
176
  noOptionsMessage={t('noOptionsFound')}
134
177
  style={{
135
178
  width: 'auto',
136
- minWidth: '92px',
137
- maxWidth: '136px',
179
+ minWidth: '120px',
180
+ maxWidth: 'min(65vw, 320px)',
138
181
  flexShrink: 0,
139
182
  }}
140
183
  options={languageOptions}
@@ -142,34 +185,35 @@ export default function PrivacyCenterLayout({
142
185
  onChange={handleLanguageChange}
143
186
  />
144
187
  <NotificationBell />
145
- <ThemeToggle />
146
188
  </div>
147
189
  </nav>
148
190
  <div style={{
149
191
  ...pcStyles.flex,
150
192
  ...pcStyles.flexCol,
151
- flexGrow: 1,
193
+ flex: 1,
194
+ minHeight: 0,
152
195
  paddingLeft: 'var(--pc-content-padding-left, 0.75rem)',
153
196
  paddingRight: 'var(--pc-content-padding-right, 0.75rem)',
154
- paddingTop: 'var(--pc-content-padding-top, 4rem)',
197
+ paddingTop: 'var(--pc-content-padding-top, 0)',
155
198
  minWidth: 0,
156
199
  backgroundColor: getVar('--pc-bg-shell'),
157
200
  }} className="pc-content-container">
158
201
  <div style={{
159
- minHeight: '100vh',
202
+ display: 'flex',
203
+ flexDirection: 'column',
204
+ flex: 1,
205
+ minHeight: 0,
160
206
  width: '100%',
161
207
  }}>
162
208
  <div style={{
163
209
  ...pcStyles.flex,
164
210
  ...pcStyles.flexCol,
165
- height: '100%',
166
- minHeight: '100vh',
211
+ flex: 1,
212
+ minHeight: 0,
167
213
  ...pcStyles.justifyBetween,
168
- ...pcStyles.gapMd,
169
- paddingLeft: '0.5rem',
170
- paddingRight: '0.5rem',
171
- paddingTop: '1rem',
172
- paddingBottom: '1rem',
214
+ ...pcStyles.gapSm,
215
+ paddingTop: '0.625rem',
216
+ paddingBottom: '0.75rem',
173
217
  }}>
174
218
  <div className={contentClassName} style={{ width: '100%' }}>{children}</div>
175
219
  <div style={{
@@ -199,17 +243,14 @@ export default function PrivacyCenterLayout({
199
243
  .pc-navbar {
200
244
  --pc-navbar-padding-left: 0.75rem;
201
245
  --pc-navbar-padding-right: 0.75rem;
202
- --pc-navbar-padding-top: 0.625rem;
203
- --pc-navbar-padding-bottom: 0.625rem;
204
- left: var(--pc-sidebar-offset) !important;
205
- width: calc(100% - var(--pc-sidebar-offset)) !important;
206
- transition: left 0.3s ease, width 0.3s ease !important;
246
+ --pc-navbar-padding-top: 0.875rem;
247
+ --pc-navbar-padding-bottom: 0.75rem;
207
248
  }
208
249
 
209
250
  .pc-content-container {
210
251
  --pc-content-padding-left: 0.75rem;
211
252
  --pc-content-padding-right: 0.75rem;
212
- --pc-content-padding-top: 4rem;
253
+ --pc-content-padding-top: 0;
213
254
  }
214
255
 
215
256
  .pc-navbar-page-meta {
@@ -247,7 +288,7 @@ export default function PrivacyCenterLayout({
247
288
  .pc-navbar {
248
289
  --pc-navbar-padding-left: 3.75rem;
249
290
  --pc-navbar-padding-right: 0.75rem;
250
- --pc-navbar-padding-top: 0.625rem;
291
+ --pc-navbar-padding-top: 0.75rem;
251
292
  --pc-navbar-padding-bottom: 0.625rem;
252
293
  }
253
294
  .pc-navbar-description {
@@ -261,21 +302,21 @@ export default function PrivacyCenterLayout({
261
302
  gap: 0.375rem;
262
303
  }
263
304
  .pc-content-container {
264
- --pc-content-padding-top: 4rem;
305
+ --pc-content-padding-top: 0;
265
306
  }
266
307
  }
267
308
  @media (min-width: 768px) {
268
309
  .pc-navbar {
269
- --pc-navbar-padding-left: 1rem;
270
- --pc-navbar-padding-right: 1rem;
271
- --pc-navbar-padding-top: 0.75rem;
310
+ --pc-navbar-padding-left: 1.25rem;
311
+ --pc-navbar-padding-right: 1.25rem;
312
+ --pc-navbar-padding-top: 0.875rem;
272
313
  --pc-navbar-padding-bottom: 0.75rem;
273
314
  }
274
315
  .pc-navbar-description {
275
316
  display: block;
276
317
  }
277
318
  .pc-content-container {
278
- --pc-content-padding-top: 4.5rem;
319
+ --pc-content-padding-top: 0;
279
320
  }
280
321
  }
281
322
  @media (min-width: 768px) {
@@ -283,19 +324,23 @@ export default function PrivacyCenterLayout({
283
324
  font-size: 1.0625rem;
284
325
  }
285
326
  .pc-navbar-description {
286
- font-size: 0.8125rem;
327
+ font-size: 0.6875rem;
287
328
  }
288
329
  .pc-content-container {
289
330
  --pc-content-padding-left: 1.25rem;
290
331
  --pc-content-padding-right: 1.25rem;
291
- --pc-content-padding-top: 4.5rem;
332
+ --pc-content-padding-top: 0;
292
333
  }
293
334
  }
294
335
  @media (min-width: 1024px) {
336
+ .pc-navbar {
337
+ --pc-navbar-padding-left: 1.5rem;
338
+ --pc-navbar-padding-right: 1.5rem;
339
+ }
295
340
  .pc-content-container {
296
341
  --pc-content-padding-left: 1.5rem;
297
342
  --pc-content-padding-right: 1.5rem;
298
- --pc-content-padding-top: 4.75rem;
343
+ --pc-content-padding-top: 0;
299
344
  }
300
345
  }
301
346
  `}</style>
@@ -18,6 +18,7 @@ const getVar = (varName: string): string => `var(${varName})`;
18
18
  import { SideMenuPC } from './SideMenuPC';
19
19
  import { useMenuItemsPC } from '../../lib/constants';
20
20
  import { useOrganization } from '../../context/OrganizationContext';
21
+ import { useBranding } from '../../context/BrandingContext';
21
22
  import type { SideBarPCProps } from './types';
22
23
  import { signOutPrivacyCenter } from '../../lib/utils';
23
24
 
@@ -34,6 +35,7 @@ export default function SideBarPC({
34
35
  const [email, setEmail] = useState<string>('');
35
36
  // const [isSigningOut, setIsSigningOut] = useState(false);
36
37
  const { organization } = useOrganization();
38
+ const branding = useBranding();
37
39
 
38
40
  useEffect(() => {
39
41
  setEmail(sessionStorage.getItem('email') || '');
@@ -109,6 +111,8 @@ export default function SideBarPC({
109
111
  }, [isMobileOpen]);
110
112
 
111
113
  const organizationName = organization?.name || '';
114
+ const displayName = branding?.brand_name || organizationName;
115
+ const isExpanded = isMobileOpen || !isCollapsed;
112
116
 
113
117
  const sidebarClassName = useMemo(
114
118
  () =>
@@ -325,7 +329,17 @@ export default function SideBarPC({
325
329
  transition: 'transform 0.3s ease',
326
330
  willChange: 'transform',
327
331
  }}>
328
- <PCAvatar name={organizationName} color="info" size="md" />
332
+ {isExpanded && branding?.logo_url ? (
333
+ <img
334
+ src={branding.logo_url}
335
+ alt={displayName}
336
+ style={{ height: '32px', width: 'auto', maxWidth: '160px', objectFit: 'contain', display: 'block' }}
337
+ />
338
+ ) : branding?.icon_url ? (
339
+ <PCAvatar src={branding.icon_url} name={displayName} color="info" size="md" />
340
+ ) : (
341
+ <PCAvatar name={displayName} color="info" size="md" />
342
+ )}
329
343
  </div>
330
344
  <div style={headerTextStyle}>
331
345
  <PCText style={{
@@ -337,7 +351,7 @@ export default function SideBarPC({
337
351
  transition: 'opacity 0.3s ease, transform 0.3s ease',
338
352
  willChange: 'opacity, transform',
339
353
  }}>
340
- {organizationName}
354
+ {displayName}
341
355
  </PCText>
342
356
  </div>
343
357
  </div>
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { CaseRequest, UserConsent } from '../lib/types';
2
+ import type { CaseRequest, ConsentModalRecord } from '../lib/types';
3
3
 
4
4
  export type TableColumnAlign = 'left' | 'center' | 'right';
5
5
 
@@ -61,7 +61,7 @@ export type ConsentManagerProps = {
61
61
  export type ModifyConsentModalProps = {
62
62
  isOpen: boolean;
63
63
  onClose: () => void;
64
- selectedRecord: UserConsent | null;
64
+ selectedRecord: ConsentModalRecord | null;
65
65
  isLoading: boolean;
66
66
  onManageConsent: (
67
67
  purposeId: string,
@@ -71,6 +71,3 @@ export type ModifyConsentModalProps = {
71
71
  ) => void;
72
72
  };
73
73
 
74
- export type UseConsentManagerColumnsProps = {
75
- onOpenModal: (record: UserConsent) => void;
76
- };
@@ -0,0 +1,44 @@
1
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
2
+ import { fetchOrgBranding } from '../api/orgBranding';
3
+ import { applyBrandingTheme, clearBrandingTheme } from '../styles/applyBrandingTheme';
4
+ import type { OrgBrandingDetail } from '../lib/types';
5
+
6
+ type BrandingContextValue = { branding: OrgBrandingDetail | null };
7
+
8
+ const BrandingContext = createContext<BrandingContextValue | null>(null);
9
+
10
+ export const BrandingProvider = ({ children, slug }: { children: ReactNode; slug: string }) => {
11
+ const [branding, setBranding] = useState<OrgBrandingDetail | null>(null);
12
+
13
+ useEffect(() => {
14
+ if (!slug) return;
15
+ let cancelled = false;
16
+ fetchOrgBranding(slug)
17
+ .then((data) => {
18
+ if (!cancelled) setBranding(data);
19
+ })
20
+ .catch((error) => {
21
+ console.warn('[RedactoPrivacyCenter] org branding fetch failed:', error);
22
+ });
23
+ return () => {
24
+ cancelled = true;
25
+ };
26
+ }, [slug]);
27
+
28
+ useEffect(() => {
29
+ if (!branding) return;
30
+ applyBrandingTheme(branding.theme);
31
+ return clearBrandingTheme;
32
+ }, [branding]);
33
+
34
+ return (
35
+ <BrandingContext.Provider value={{ branding }}>
36
+ {children}
37
+ </BrandingContext.Provider>
38
+ );
39
+ };
40
+
41
+ export const useBranding = (): OrgBrandingDetail | null => {
42
+ const ctx = useContext(BrandingContext);
43
+ return ctx?.branding ?? null;
44
+ };