@redacto.io/consent-sdk-react 6.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,5 @@
1
1
  import { useEffect, useState, useRef, type ReactNode } from 'react';
2
2
  import { OrganizationProvider } from './context/OrganizationContext';
3
- import { ThemeProvider } from './context/theme';
4
3
  import type { OrganizationDetail } from './lib/types';
5
4
  import {
6
5
  checkAndRefreshAuth,
@@ -14,12 +13,11 @@ import PrivacyCenterLayoutComponent from './components/PrivacyCenterLayout';
14
13
 
15
14
  interface PrivacyCenterLayoutProps {
16
15
  children: ReactNode;
17
- defaultTheme?: 'light' | 'dark';
18
16
  slug: string;
19
17
  onBack?: 'back' | 'signout';
20
18
  }
21
19
 
22
- export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: PrivacyCenterLayoutProps) {
20
+ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLayoutProps) {
23
21
  const { consentClient } = useBaseUrl();
24
22
  // Read accessToken from AuthContext so this effect re-runs after AuthProvider
25
23
  // syncs JWT data to sessionStorage (fixes race condition where child effects
@@ -162,12 +160,10 @@ export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: Pr
162
160
  }
163
161
 
164
162
  return (
165
- <ThemeProvider defaultTheme={defaultTheme}>
166
- <OrganizationProvider organization={organization}>
167
- <PrivacyCenterLayoutComponent onBack={onBack}>
168
- {children}
169
- </PrivacyCenterLayoutComponent>
170
- </OrganizationProvider>
171
- </ThemeProvider>
163
+ <OrganizationProvider organization={organization}>
164
+ <PrivacyCenterLayoutComponent onBack={onBack}>
165
+ {children}
166
+ </PrivacyCenterLayoutComponent>
167
+ </OrganizationProvider>
172
168
  );
173
169
  }
@@ -7,6 +7,7 @@ import { AuthProvider } from './context/AuthContext';
7
7
  import { NavigationProvider } from './context/NavigationContext';
8
8
  import type { InternalPage } from './context/NavigationContext';
9
9
  import { PageChromeProvider } from './context/PageChromeContext';
10
+ import { BrandingProvider } from './context/BrandingContext';
10
11
  import { injectPCStyles } from './styles/injectStyles';
11
12
 
12
13
  // Inject Privacy Center CSS styles on module load
@@ -15,7 +16,6 @@ injectPCStyles();
15
16
  interface RedactoPrivacyCenterProps {
16
17
  baseUrl: string; // Consent server base URL
17
18
  slug: string;
18
- theme?: 'light' | 'dark';
19
19
  accessToken: string;
20
20
  refreshToken: string;
21
21
  onError: (error: Error) => boolean;
@@ -26,7 +26,6 @@ interface RedactoPrivacyCenterProps {
26
26
  export function RedactoPrivacyCenter({
27
27
  baseUrl,
28
28
  slug,
29
- theme = 'light',
30
29
  accessToken,
31
30
  refreshToken,
32
31
  onError,
@@ -35,21 +34,23 @@ export function RedactoPrivacyCenter({
35
34
  }: RedactoPrivacyCenterProps) {
36
35
  return (
37
36
  <I18nextProvider i18n={i18n}>
38
- <BaseUrlProvider baseUrl={baseUrl}>
39
- <AuthProvider
40
- accessToken={accessToken}
41
- refreshToken={refreshToken}
42
- onError={onError}
43
- >
44
- <NavigationProvider initialPage={initialPage}>
45
- <PageChromeProvider>
46
- <PrivacyCenterLayout defaultTheme={theme} slug={slug} onBack={onBack}>
47
- <PrivacyCenterHomePage />
48
- </PrivacyCenterLayout>
49
- </PageChromeProvider>
50
- </NavigationProvider>
51
- </AuthProvider>
52
- </BaseUrlProvider>
37
+ <BrandingProvider slug={slug}>
38
+ <BaseUrlProvider baseUrl={baseUrl}>
39
+ <AuthProvider
40
+ accessToken={accessToken}
41
+ refreshToken={refreshToken}
42
+ onError={onError}
43
+ >
44
+ <NavigationProvider initialPage={initialPage}>
45
+ <PageChromeProvider>
46
+ <PrivacyCenterLayout slug={slug} onBack={onBack}>
47
+ <PrivacyCenterHomePage />
48
+ </PrivacyCenterLayout>
49
+ </PageChromeProvider>
50
+ </NavigationProvider>
51
+ </AuthProvider>
52
+ </BaseUrlProvider>
53
+ </BrandingProvider>
53
54
  </I18nextProvider>
54
55
  );
55
56
  }
@@ -0,0 +1,9 @@
1
+ import { apiFetch } from './fetcher';
2
+ import type { OrgBrandingDetail } from '../lib/types';
3
+
4
+ const USER_SERVER_BASE_URL = 'https://api.redacto.tech/user/api/0';
5
+
6
+ export async function fetchOrgBranding(slug: string): Promise<OrgBrandingDetail> {
7
+ const url = `${USER_SERVER_BASE_URL}/public/org-branding/by-slug/${encodeURIComponent(slug)}`;
8
+ return apiFetch<OrgBrandingDetail>(url, { method: 'GET' });
9
+ }
@@ -447,12 +447,17 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
447
447
  );
448
448
  setConsentRows(filteredRows);
449
449
 
450
- // Extract unique products from all groups
451
- const combinedGroups = [
452
- ...(response.direct || []),
453
- ...(response.nominated || []),
454
- ];
455
- setProductOptions(buildProductOptionsFromGroups(combinedGroups));
450
+ // Only refresh the product filter options from an unfiltered fetch.
451
+ // When a product filter is active the API returns rows for that one
452
+ // product, so re-deriving options here would shrink the dropdown to
453
+ // just the selected product (forcing the user to refresh to recover).
454
+ if (!productFilter) {
455
+ const combinedGroups = [
456
+ ...(response.direct || []),
457
+ ...(response.nominated || []),
458
+ ];
459
+ setProductOptions(buildProductOptionsFromGroups(combinedGroups));
460
+ }
456
461
  } catch (error) {
457
462
  console.error(error);
458
463
  setConsentDetail(null);
@@ -1122,14 +1127,11 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
1122
1127
  </>
1123
1128
  )}
1124
1129
 
1125
- {/* Always show pagination, even during loading */}
1126
- <div className="pc-consent-pagination-wrap">
1127
- {viewMode === 'product' ? (
1130
+ {viewMode === 'product' && (
1131
+ <div className="pc-consent-pagination-wrap">
1128
1132
  <AdvancedTablePagination table={productTable} />
1129
- ) : (
1130
- <AdvancedTablePagination table={purposeTable} />
1131
- )}
1132
- </div>
1133
+ </div>
1134
+ )}
1133
1135
  </div>
1134
1136
 
1135
1137
  <PCModal
@@ -1153,7 +1155,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
1153
1155
  }
1154
1156
  footer={
1155
1157
  <div className="pc-modal-footer-actions">
1156
- <PCButton variant="outline" onClick={() => setExportModal(false)}>
1158
+ <PCButton variant="secondary" onClick={() => setExportModal(false)}>
1157
1159
  {t('cancel')}
1158
1160
  </PCButton>
1159
1161
  <PCButton variant="primary" onClick={handleExport}>
@@ -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
  >
@@ -300,7 +300,7 @@ export const ModifyConsentModal = ({
300
300
  ...pcStyles.gapSm,
301
301
  ...pcStyles.justifyEnd,
302
302
  }}>
303
- <PCButton variant="outline" onClick={onClose}>
303
+ <PCButton variant="secondary" onClick={onClose}>
304
304
  {t('cancel')}
305
305
  </PCButton>
306
306
  <PCButton
@@ -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';
@@ -42,7 +40,6 @@ export default function PrivacyCenterLayout({
42
40
  return (
43
41
  <LocaleContext.Provider value={{ locale, setLocale }}>
44
42
  <Suspense fallback={<div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.justifyCenter, minHeight: '100vh' }}>{t('loadingEllipsis')}</div>}>
45
- <ThemeTransitionOverlay />
46
43
  <main
47
44
  className="pc-root"
48
45
  style={{
@@ -188,7 +185,6 @@ export default function PrivacyCenterLayout({
188
185
  onChange={handleLanguageChange}
189
186
  />
190
187
  <NotificationBell />
191
- <ThemeToggle className="pc-navbar-icon-btn" />
192
188
  </div>
193
189
  </nav>
194
190
  <div 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>
@@ -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
+ };
@@ -27,6 +27,28 @@ export type OrganizationResponse = {
27
27
  detail: OrganizationDetail;
28
28
  };
29
29
 
30
+ export type OrgBrandingTheme = {
31
+ primary: string;
32
+ secondary: string;
33
+ primary_contrast: string;
34
+ link: string;
35
+ surface: string;
36
+ text: string;
37
+ text_muted: string;
38
+ border: string;
39
+ success: string;
40
+ warning: string;
41
+ danger: string;
42
+ };
43
+
44
+ export type OrgBrandingDetail = {
45
+ logo_url: string | null;
46
+ icon_url: string | null;
47
+ brand_name: string;
48
+ theme: OrgBrandingTheme;
49
+ hide_platform_branding: boolean;
50
+ };
51
+
30
52
  export type DataElement = {
31
53
  uuid: string;
32
54
  name: string;
@@ -0,0 +1,66 @@
1
+ import type { OrgBrandingTheme } from '../lib/types';
2
+
3
+ const OVERRIDDEN_VARS = [
4
+ '--pc-primary',
5
+ '--pc-primary-hover',
6
+ '--pc-primary-active',
7
+ '--pc-info',
8
+ '--pc-border-color-focus',
9
+ '--pc-secondary',
10
+ '--pc-secondary-hover',
11
+ '--pc-text-inverse',
12
+ '--pc-bg-shell',
13
+ '--pc-bg-primary',
14
+ '--pc-text-primary',
15
+ '--pc-text-secondary',
16
+ '--pc-border-color',
17
+ '--pc-success',
18
+ '--pc-warning',
19
+ '--pc-error',
20
+ ] as const;
21
+
22
+ function darken(hex: string, amount: number): string {
23
+ const h = hex.replace('#', '').trim();
24
+ if (h.length !== 6) return hex;
25
+ const num = parseInt(h, 16);
26
+ if (Number.isNaN(num)) return hex;
27
+ const r = Math.max(0, ((num >> 16) & 0xff) - Math.round(255 * amount));
28
+ const g = Math.max(0, ((num >> 8) & 0xff) - Math.round(255 * amount));
29
+ const b = Math.max(0, (num & 0xff) - Math.round(255 * amount));
30
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
31
+ }
32
+
33
+ export function applyBrandingTheme(theme: OrgBrandingTheme): void {
34
+ if (typeof document === 'undefined') return;
35
+ const root = document.documentElement;
36
+ const set = (prop: string, value: string) => root.style.setProperty(prop, value);
37
+
38
+ set('--pc-primary', theme.primary);
39
+ set('--pc-primary-hover', darken(theme.primary, 0.08));
40
+ set('--pc-primary-active', darken(theme.primary, 0.16));
41
+ set('--pc-info', theme.primary);
42
+ set('--pc-border-color-focus', theme.primary);
43
+
44
+ set('--pc-secondary', theme.secondary);
45
+ set('--pc-secondary-hover', darken(theme.secondary, 0.08));
46
+
47
+ set('--pc-text-inverse', theme.primary_contrast);
48
+
49
+ set('--pc-bg-shell', theme.surface);
50
+ set('--pc-bg-primary', theme.surface);
51
+
52
+ set('--pc-text-primary', theme.text);
53
+ set('--pc-text-secondary', theme.text_muted);
54
+
55
+ set('--pc-border-color', theme.border);
56
+
57
+ set('--pc-success', theme.success);
58
+ set('--pc-warning', theme.warning);
59
+ set('--pc-error', theme.danger);
60
+ }
61
+
62
+ export function clearBrandingTheme(): void {
63
+ if (typeof document === 'undefined') return;
64
+ const root = document.documentElement;
65
+ for (const prop of OVERRIDDEN_VARS) root.style.removeProperty(prop);
66
+ }
@@ -32,7 +32,7 @@ export const injectPCStyles = () => {
32
32
  // CSS variables will be injected via a separate import or inline
33
33
  // For now, we'll rely on the host app or inject them here
34
34
  variablesStyle.textContent = `
35
- :root, [data-theme="light"] {
35
+ :root {
36
36
  --pc-primary: #3b82f6;
37
37
  --pc-primary-hover: #2563eb;
38
38
  --pc-primary-active: #1d4ed8;
@@ -107,39 +107,6 @@ export const injectPCStyles = () => {
107
107
  --pc-sidebar-expanded-width: 300px;
108
108
  --pc-sidebar-expanded-width-2xl: 320px;
109
109
  }
110
- [data-theme="dark"] {
111
- --pc-primary: #60a5fa;
112
- --pc-primary-hover: #3b82f6;
113
- --pc-primary-active: #2563eb;
114
- --pc-secondary: #9ca3af;
115
- --pc-secondary-hover: #6b7280;
116
- --pc-success: #34d399;
117
- --pc-error: #f87171;
118
- --pc-warning: #fbbf24;
119
- --pc-warning-bg: #78350f;
120
- --pc-warning-border: #fbbf24;
121
- --pc-warning-text: #fef3c7;
122
- --pc-info: #60a5fa;
123
- --pc-bg-shell: #111827;
124
- --pc-bg-primary: #1b2435;
125
- --pc-bg-secondary: #243044;
126
- --pc-bg-tertiary: #31405a;
127
- --pc-bg-hover: #243044;
128
- --pc-bg-active: #31405a;
129
- --pc-bg-overlay: rgba(0, 0, 0, 0.7);
130
- --pc-text-primary: #f9fafb;
131
- --pc-text-secondary: #d1d5db;
132
- --pc-text-tertiary: #9ca3af;
133
- --pc-text-inverse: #111827;
134
- --pc-text-disabled: #6b7280;
135
- --pc-border-color: rgba(148, 163, 184, 0.18);
136
- --pc-border-color-hover: rgba(148, 163, 184, 0.28);
137
- --pc-border-color-focus: #60a5fa;
138
- --pc-sidebar-shadow: 14px 0 36px rgba(2, 6, 23, 0.42), 1px 0 0 rgba(148, 163, 184, 0.08);
139
- --pc-icon-filter: 1;
140
- --pc-consent-toolbar-control-height: 2rem;
141
- --pc-navbar-actions-control-height: var(--pc-consent-toolbar-control-height);
142
- }
143
110
  `;
144
111
  document.head.appendChild(variablesStyle);
145
112
  }
@@ -170,82 +137,6 @@ export const injectPCStyles = () => {
170
137
  transition: none !important;
171
138
  }
172
139
 
173
- /* Sun and Moon Rising Animation */
174
- @keyframes pc-sun-moon-rise {
175
- 0% {
176
- bottom: -150px;
177
- opacity: 0;
178
- transform: translateX(-50%) scale(0.5) rotate(-10deg);
179
- }
180
- 15% {
181
- opacity: 1;
182
- transform: translateX(-50%) scale(1) rotate(0deg);
183
- }
184
- 70% {
185
- opacity: 1;
186
- transform: translateX(-50%) scale(1.1) rotate(5deg);
187
- }
188
- 100% {
189
- bottom: calc(100vh + 50px);
190
- opacity: 0;
191
- transform: translateX(-50%) scale(0.7) rotate(10deg);
192
- }
193
- }
194
-
195
- /* Sun specific glow animation */
196
- @keyframes pc-sun-glow {
197
- 0%, 100% {
198
- filter: drop-shadow(0 0 30px rgba(251, 191, 36, 0.7))
199
- drop-shadow(0 0 60px rgba(251, 191, 36, 0.4));
200
- }
201
- 50% {
202
- filter: drop-shadow(0 0 50px rgba(251, 191, 36, 0.9))
203
- drop-shadow(0 0 100px rgba(251, 191, 36, 0.6));
204
- }
205
- }
206
-
207
- /* Moon specific glow animation */
208
- @keyframes pc-moon-glow {
209
- 0%, 100% {
210
- filter: drop-shadow(0 0 30px rgba(224, 231, 255, 0.7))
211
- drop-shadow(0 0 60px rgba(224, 231, 255, 0.4));
212
- }
213
- 50% {
214
- filter: drop-shadow(0 0 50px rgba(224, 231, 255, 0.9))
215
- drop-shadow(0 0 100px rgba(224, 231, 255, 0.6));
216
- }
217
- }
218
-
219
- .pc-theme-transition-overlay {
220
- position: fixed;
221
- top: 0;
222
- left: 0;
223
- right: 0;
224
- bottom: 0;
225
- pointer-events: none;
226
- z-index: 9999;
227
- display: flex;
228
- align-items: center;
229
- justify-content: center;
230
- opacity: 0;
231
- will-change: opacity;
232
- overflow: hidden;
233
- }
234
-
235
- .pc-theme-transition-icon {
236
- will-change: transform, opacity, bottom, filter;
237
- }
238
-
239
- .pc-theme-transition-light {
240
- animation: pc-sun-moon-rise 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards,
241
- pc-sun-glow 1s ease-in-out;
242
- }
243
-
244
- .pc-theme-transition-dark {
245
- animation: pc-sun-moon-rise 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards,
246
- pc-moon-glow 1s ease-in-out;
247
- }
248
-
249
140
  /* Keyframe Animations */
250
141
  @keyframes pc-spin {
251
142
  0% {
@@ -365,7 +256,8 @@ export const injectPCStyles = () => {
365
256
  }
366
257
 
367
258
  .pc-button-secondary:hover:not(:disabled) {
368
- background-color: var(--pc-secondary-hover) !important;
259
+ background-color: var(--pc-primary) !important;
260
+ color: var(--pc-text-inverse) !important;
369
261
  }
370
262
 
371
263
  .pc-button-outline:hover:not(:disabled) {
@@ -204,8 +204,9 @@ export const pcStyles = {
204
204
  backgroundColor: getVar('--pc-primary-hover'),
205
205
  },
206
206
  buttonSecondary: {
207
- backgroundColor: getVar('--pc-secondary'),
208
- color: getVar('--pc-text-inverse'),
207
+ backgroundColor: 'transparent',
208
+ color: getVar('--pc-primary'),
209
+ border: `1px solid ${getVar('--pc-primary')}`,
209
210
  },
210
211
  buttonOutline: {
211
212
  backgroundColor: 'transparent',
@@ -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;
@@ -95,44 +94,3 @@
95
94
  /* Header: language, notifications, theme (same pixel row as compact controls) */
96
95
  --pc-navbar-actions-control-height: var(--pc-consent-toolbar-control-height);
97
96
  }
98
-
99
- [data-theme="dark"] {
100
- /* Colors */
101
- --pc-primary: #60a5fa;
102
- --pc-primary-hover: #3b82f6;
103
- --pc-primary-active: #2563eb;
104
- --pc-secondary: #9ca3af;
105
- --pc-secondary-hover: #6b7280;
106
- --pc-success: #34d399;
107
- --pc-error: #f87171;
108
- --pc-warning: #fbbf24;
109
- --pc-warning-bg: #78350f;
110
- --pc-warning-border: #fbbf24;
111
- --pc-warning-text: #fef3c7;
112
- --pc-info: #60a5fa;
113
-
114
- /* Backgrounds */
115
- --pc-bg-shell: #111827;
116
- --pc-bg-primary: #1b2435;
117
- --pc-bg-secondary: #243044;
118
- --pc-bg-tertiary: #31405a;
119
- --pc-bg-hover: #243044;
120
- --pc-bg-active: #31405a;
121
- --pc-bg-overlay: rgba(0, 0, 0, 0.7);
122
-
123
- /* Text */
124
- --pc-text-primary: #f9fafb;
125
- --pc-text-secondary: #d1d5db;
126
- --pc-text-tertiary: #9ca3af;
127
- --pc-text-inverse: #111827;
128
- --pc-text-disabled: #6b7280;
129
-
130
- /* Borders */
131
- --pc-border-color: rgba(148, 163, 184, 0.18);
132
- --pc-border-color-hover: rgba(148, 163, 184, 0.28);
133
- --pc-border-color-focus: #60a5fa;
134
- --pc-sidebar-shadow: 14px 0 36px rgba(2, 6, 23, 0.42), 1px 0 0 rgba(148, 163, 184, 0.08);
135
-
136
- /* Icon filters for dark theme */
137
- --pc-icon-filter: 1;
138
- }
@@ -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