@redacto.io/consent-sdk-react 6.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +25 -24
- package/CHANGELOG.md +23 -0
- package/dist/index.js +228 -166
- package/dist/index.mjs +228 -166
- package/dist/privacy-center.d.mts +2 -4
- package/dist/privacy-center.d.ts +2 -4
- package/dist/privacy-center.js +1876 -2079
- package/dist/privacy-center.mjs +1470 -1679
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +209 -155
- package/src/RedactoNoticeConsent/styles.ts +10 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -10
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +18 -17
- package/src/RedactoPrivacyCenter/api/orgBranding.ts +9 -0
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +16 -14
- package/src/RedactoPrivacyCenter/components/Form.tsx +3 -3
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +0 -4
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +16 -2
- package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +44 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +22 -0
- package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +66 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +3 -111
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +3 -2
- package/src/RedactoPrivacyCenter/styles/variables.css +1 -43
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -10
- package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +0 -85
- package/src/RedactoPrivacyCenter/context/theme.tsx +0 -127
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +0 -70
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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
|
|
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-
|
|
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:
|
|
208
|
-
color: getVar('--pc-
|
|
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 {
|
|
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
|
|
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
|
|
@@ -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
|
-
};
|