@redacto.io/consent-sdk-react 7.3.0 → 8.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/CHANGELOG.md +6 -0
- package/dist/chunk-6A665OPR.mjs +30 -0
- package/dist/index.mjs +85 -108
- package/dist/privacy-center.js +3517 -3281
- package/dist/privacy-center.mjs +3519 -3295
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +3 -3
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +204 -0
- package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +218 -0
- package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +205 -0
- package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +107 -0
- package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +100 -0
- package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +338 -0
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +222 -0
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +28 -0
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +40 -0
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +62 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +5 -27
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -19
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +42 -5
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +13 -2
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +0 -350
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +0 -489
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +0 -408
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +0 -1
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +0 -38
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState, type CSSProperties } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ArrowLeft, ChevronDown, LogOut } from 'lucide-react';
|
|
4
|
+
import { PCAvatar, PCPopover } from '../../ui';
|
|
5
|
+
import { useBranding } from '../../context/BrandingContext';
|
|
6
|
+
import { useOrganization } from '../../context/OrganizationContext';
|
|
7
|
+
import { getVar, signOutPrivacyCenter } from '../../lib/utils';
|
|
8
|
+
|
|
9
|
+
type UserMenuProps = {
|
|
10
|
+
onBack?: 'back' | 'signout';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function readEmail(): string {
|
|
14
|
+
if (typeof window === 'undefined') return '';
|
|
15
|
+
try {
|
|
16
|
+
return window.sessionStorage.getItem('email') ?? '';
|
|
17
|
+
} catch {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function initialsFromEmail(email: string): string {
|
|
23
|
+
if (!email) return '?';
|
|
24
|
+
const handle = email.split('@')[0] ?? '';
|
|
25
|
+
const parts = handle.split(/[._-]+/).filter(Boolean);
|
|
26
|
+
if (parts.length >= 2) {
|
|
27
|
+
return (parts[0]![0]! + parts[1]![0]!).toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
return (handle[0] ?? '?').toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function UserMenu({ onBack }: UserMenuProps) {
|
|
33
|
+
const { t } = useTranslation();
|
|
34
|
+
const branding = useBranding();
|
|
35
|
+
const { organization } = useOrganization();
|
|
36
|
+
const [email, setEmail] = useState('');
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setEmail(readEmail());
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleAction = useCallback(() => {
|
|
43
|
+
if (onBack === 'back') {
|
|
44
|
+
window.history.back();
|
|
45
|
+
} else if (onBack === 'signout') {
|
|
46
|
+
signOutPrivacyCenter();
|
|
47
|
+
window.history.back();
|
|
48
|
+
}
|
|
49
|
+
}, [onBack]);
|
|
50
|
+
|
|
51
|
+
const actionLabel = onBack === 'back' ? t('back') : t('signOut');
|
|
52
|
+
const ActionIcon = onBack === 'back' ? ArrowLeft : LogOut;
|
|
53
|
+
|
|
54
|
+
const displayName = branding?.brand_name || organization?.name || '';
|
|
55
|
+
const triggerAvatarName = email ? initialsFromEmail(email) : displayName || '?';
|
|
56
|
+
|
|
57
|
+
const triggerStyle: CSSProperties = {
|
|
58
|
+
display: 'inline-flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
gap: 6,
|
|
61
|
+
padding: '4px 6px 4px 4px',
|
|
62
|
+
borderRadius: 9999,
|
|
63
|
+
background: 'transparent',
|
|
64
|
+
border: `1px solid ${getVar('--pc-border-color')}`,
|
|
65
|
+
color: getVar('--pc-text-secondary'),
|
|
66
|
+
cursor: 'pointer',
|
|
67
|
+
transition: 'background 150ms ease, border-color 150ms ease, box-shadow 150ms ease',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const trigger = (
|
|
71
|
+
<div
|
|
72
|
+
style={triggerStyle}
|
|
73
|
+
role="presentation"
|
|
74
|
+
onMouseEnter={(e) => {
|
|
75
|
+
(e.currentTarget as HTMLDivElement).style.boxShadow =
|
|
76
|
+
'0 0 0 2px color-mix(in srgb, var(--pc-primary) 18%, transparent)';
|
|
77
|
+
}}
|
|
78
|
+
onMouseLeave={(e) => {
|
|
79
|
+
(e.currentTarget as HTMLDivElement).style.boxShadow = 'none';
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{branding?.icon_url ? (
|
|
83
|
+
<PCAvatar src={branding.icon_url} name={triggerAvatarName} size="sm" color="info" />
|
|
84
|
+
) : (
|
|
85
|
+
<PCAvatar name={triggerAvatarName} size="sm" color="info" />
|
|
86
|
+
)}
|
|
87
|
+
<ChevronDown size={14} strokeWidth={2} />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const content = (
|
|
92
|
+
<div
|
|
93
|
+
role="menu"
|
|
94
|
+
style={{
|
|
95
|
+
minWidth: 280,
|
|
96
|
+
maxWidth: 320,
|
|
97
|
+
padding: 0,
|
|
98
|
+
background: getVar('--pc-bg-primary'),
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{/* Branded gradient header */}
|
|
102
|
+
<div
|
|
103
|
+
className="pc-shell-user-menu-header"
|
|
104
|
+
style={{
|
|
105
|
+
padding: '14px 18px',
|
|
106
|
+
display: 'flex',
|
|
107
|
+
flexDirection: 'column',
|
|
108
|
+
gap: 4,
|
|
109
|
+
borderBottom: `1px solid ${getVar('--pc-border-color')}`,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<div
|
|
113
|
+
style={{
|
|
114
|
+
fontSize: 10,
|
|
115
|
+
fontWeight: 600,
|
|
116
|
+
letterSpacing: '0.1em',
|
|
117
|
+
textTransform: 'uppercase',
|
|
118
|
+
color: getVar('--pc-text-secondary'),
|
|
119
|
+
lineHeight: 1.2,
|
|
120
|
+
opacity: 0.85,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Privacy Center
|
|
124
|
+
</div>
|
|
125
|
+
<div
|
|
126
|
+
title={displayName}
|
|
127
|
+
style={{
|
|
128
|
+
fontSize: 16,
|
|
129
|
+
fontWeight: 600,
|
|
130
|
+
letterSpacing: '-0.01em',
|
|
131
|
+
color: getVar('--pc-text-primary'),
|
|
132
|
+
lineHeight: 1.25,
|
|
133
|
+
overflow: 'hidden',
|
|
134
|
+
textOverflow: 'ellipsis',
|
|
135
|
+
whiteSpace: 'nowrap',
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{displayName || '—'}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Identity card */}
|
|
143
|
+
{email && (
|
|
144
|
+
<div
|
|
145
|
+
style={{
|
|
146
|
+
padding: '12px 16px',
|
|
147
|
+
display: 'flex',
|
|
148
|
+
alignItems: 'center',
|
|
149
|
+
gap: 10,
|
|
150
|
+
borderBottom: onBack ? `1px solid ${getVar('--pc-border-color')}` : 'none',
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<PCAvatar name={initialsFromEmail(email)} size="sm" color="info" />
|
|
154
|
+
<div
|
|
155
|
+
title={email}
|
|
156
|
+
style={{
|
|
157
|
+
fontSize: 13,
|
|
158
|
+
fontWeight: 600,
|
|
159
|
+
color: getVar('--pc-text-primary'),
|
|
160
|
+
lineHeight: 1.3,
|
|
161
|
+
overflow: 'hidden',
|
|
162
|
+
textOverflow: 'ellipsis',
|
|
163
|
+
whiteSpace: 'nowrap',
|
|
164
|
+
minWidth: 0,
|
|
165
|
+
flex: 1,
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{email}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Action row */}
|
|
174
|
+
{onBack && (
|
|
175
|
+
<div style={{ padding: '6px 8px 8px' }}>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
role="menuitem"
|
|
179
|
+
onClick={handleAction}
|
|
180
|
+
style={{
|
|
181
|
+
display: 'flex',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
gap: 10,
|
|
184
|
+
width: '100%',
|
|
185
|
+
padding: '10px 14px',
|
|
186
|
+
border: 'none',
|
|
187
|
+
borderRadius: 8,
|
|
188
|
+
background: 'transparent',
|
|
189
|
+
color: getVar('--pc-text-primary'),
|
|
190
|
+
cursor: 'pointer',
|
|
191
|
+
fontFamily: 'inherit',
|
|
192
|
+
fontSize: 14,
|
|
193
|
+
fontWeight: 500,
|
|
194
|
+
textAlign: 'left',
|
|
195
|
+
transition: 'background 150ms ease',
|
|
196
|
+
}}
|
|
197
|
+
onMouseEnter={(e) => {
|
|
198
|
+
e.currentTarget.style.background = getVar('--pc-bg-hover');
|
|
199
|
+
}}
|
|
200
|
+
onMouseLeave={(e) => {
|
|
201
|
+
e.currentTarget.style.background = 'transparent';
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
<ActionIcon size={16} strokeWidth={2} />
|
|
205
|
+
<span>{actionLabel}</span>
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<PCPopover
|
|
214
|
+
placement="bottom-end"
|
|
215
|
+
shadow="md"
|
|
216
|
+
className="pc-shell-user-menu-popover"
|
|
217
|
+
content={content}
|
|
218
|
+
>
|
|
219
|
+
{trigger}
|
|
220
|
+
</PCPopover>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect, useState, type RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useIsTruncated<T extends HTMLElement>(ref: RefObject<T>): boolean {
|
|
4
|
+
const [truncated, setTruncated] = useState(false);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const el = ref.current;
|
|
8
|
+
if (!el) return;
|
|
9
|
+
|
|
10
|
+
const check = () => {
|
|
11
|
+
setTruncated(el.scrollWidth > el.clientWidth + 1);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
check();
|
|
15
|
+
|
|
16
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
17
|
+
const onResize = () => check();
|
|
18
|
+
window.addEventListener('resize', onResize);
|
|
19
|
+
return () => window.removeEventListener('resize', onResize);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ro = new ResizeObserver(check);
|
|
23
|
+
ro.observe(el);
|
|
24
|
+
return () => ro.disconnect();
|
|
25
|
+
}, [ref]);
|
|
26
|
+
|
|
27
|
+
return truncated;
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ShortcutCombo = {
|
|
4
|
+
key: string;
|
|
5
|
+
/** When true, requires Cmd (mac) or Ctrl (win/linux). */
|
|
6
|
+
mod?: boolean;
|
|
7
|
+
shift?: boolean;
|
|
8
|
+
alt?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
12
|
+
if (!target || !(target instanceof HTMLElement)) return false;
|
|
13
|
+
const tag = target.tagName;
|
|
14
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
15
|
+
if (target.isContentEditable) return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useKeyboardShortcut(combo: ShortcutCombo, handler: () => void) {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (typeof window === 'undefined') return;
|
|
22
|
+
|
|
23
|
+
const onKey = (event: KeyboardEvent) => {
|
|
24
|
+
if (event.key.toLowerCase() !== combo.key.toLowerCase()) return;
|
|
25
|
+
if (combo.mod && !(event.metaKey || event.ctrlKey)) return;
|
|
26
|
+
if (!combo.mod && (event.metaKey || event.ctrlKey)) return;
|
|
27
|
+
if (combo.shift && !event.shiftKey) return;
|
|
28
|
+
if (!combo.shift && event.shiftKey) return;
|
|
29
|
+
if (combo.alt && !event.altKey) return;
|
|
30
|
+
if (!combo.alt && event.altKey) return;
|
|
31
|
+
if (isEditableTarget(event.target)) return;
|
|
32
|
+
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
handler();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
window.addEventListener('keydown', onKey);
|
|
38
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
39
|
+
}, [combo.key, combo.mod, combo.shift, combo.alt, handler]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SidebarState = 'expanded' | 'collapsed';
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = 'redacto-pc-sidebar-state';
|
|
6
|
+
const EXPAND_MIN_WIDTH = '(min-width: 1024px)';
|
|
7
|
+
|
|
8
|
+
function getDefaultState(): SidebarState {
|
|
9
|
+
if (typeof window === 'undefined') return 'collapsed';
|
|
10
|
+
try {
|
|
11
|
+
return window.matchMedia(EXPAND_MIN_WIDTH).matches ? 'expanded' : 'collapsed';
|
|
12
|
+
} catch {
|
|
13
|
+
return 'collapsed';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readStored(): SidebarState | null {
|
|
18
|
+
if (typeof window === 'undefined') return null;
|
|
19
|
+
try {
|
|
20
|
+
const value = window.localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
return value === 'expanded' || value === 'collapsed' ? value : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeStored(value: SidebarState) {
|
|
28
|
+
if (typeof window === 'undefined') return;
|
|
29
|
+
try {
|
|
30
|
+
window.localStorage.setItem(STORAGE_KEY, value);
|
|
31
|
+
} catch {
|
|
32
|
+
// localStorage unavailable / quota exceeded — ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useSidebarState() {
|
|
37
|
+
const [state, setStateRaw] = useState<SidebarState | undefined>(undefined);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setStateRaw(readStored() ?? getDefaultState());
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (state) writeStored(state);
|
|
45
|
+
}, [state]);
|
|
46
|
+
|
|
47
|
+
const setState = useCallback((next: SidebarState) => {
|
|
48
|
+
setStateRaw(next);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const toggle = useCallback(() => {
|
|
52
|
+
setStateRaw((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
state: state ?? 'collapsed',
|
|
57
|
+
isHydrated: state !== undefined,
|
|
58
|
+
isExpanded: state === 'expanded',
|
|
59
|
+
setState,
|
|
60
|
+
toggle,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,42 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
IoPulseOutline,
|
|
4
|
-
IoShieldCheckmarkOutline,
|
|
5
|
-
} from 'react-icons/io5';
|
|
6
|
-
import type { BadgeType, MenuItem } from './types';
|
|
1
|
+
import { Activity, MessageCircleWarning, ShieldCheck } from 'lucide-react';
|
|
2
|
+
import type { MenuItem } from './types';
|
|
7
3
|
|
|
8
4
|
export const menuItemsPC: MenuItem[] = [
|
|
9
5
|
{
|
|
10
6
|
name: 'Consent Manager',
|
|
11
7
|
href: 'consent-manager',
|
|
12
|
-
icon: <
|
|
8
|
+
icon: <ShieldCheck size={18} strokeWidth={1.75} />,
|
|
13
9
|
},
|
|
14
10
|
{
|
|
15
11
|
name: 'Grievance',
|
|
16
12
|
href: 'form',
|
|
17
|
-
icon: <
|
|
13
|
+
icon: <MessageCircleWarning size={18} strokeWidth={1.75} />,
|
|
18
14
|
},
|
|
19
15
|
{
|
|
20
16
|
name: 'Activity',
|
|
21
17
|
href: 'activity',
|
|
22
|
-
icon: <
|
|
18
|
+
icon: <Activity size={18} strokeWidth={1.75} />,
|
|
23
19
|
},
|
|
24
20
|
];
|
|
25
|
-
|
|
26
|
-
export const badges: Record<BadgeType, { text: string; className: string }> = {
|
|
27
|
-
new: {
|
|
28
|
-
text: 'New',
|
|
29
|
-
className: 'bg-blue-100 text-blue-500',
|
|
30
|
-
},
|
|
31
|
-
coming: {
|
|
32
|
-
text: '',
|
|
33
|
-
className: 'visibility-hidden',
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const LOCAL_STORAGE_KEYS_TO_REMOVE = [
|
|
38
|
-
'email',
|
|
39
|
-
'access_token',
|
|
40
|
-
'i18nextLng',
|
|
41
|
-
'lang',
|
|
42
|
-
] as const;
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
2
|
|
|
3
|
-
export type BadgeType = 'new' | 'coming';
|
|
4
|
-
|
|
5
|
-
export type MenuBadgeProps = {
|
|
6
|
-
type: BadgeType;
|
|
7
|
-
isDisabled?: boolean;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
3
|
export type DropdownItem = {
|
|
11
4
|
name: string;
|
|
12
5
|
href: string;
|
|
@@ -26,15 +19,3 @@ export type MenuItem = {
|
|
|
26
19
|
badge?: string;
|
|
27
20
|
dropdownItems?: DropdownItem[];
|
|
28
21
|
};
|
|
29
|
-
|
|
30
|
-
export type SideMenuPCProps = {
|
|
31
|
-
menuItems: MenuItem[];
|
|
32
|
-
isCollapsed: boolean;
|
|
33
|
-
onItemSelect?: () => void;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type SideBarPCProps = {
|
|
37
|
-
className?: string;
|
|
38
|
-
isCompact?: boolean;
|
|
39
|
-
onBack?: 'back' | 'signout';
|
|
40
|
-
};
|
|
@@ -77,7 +77,7 @@ export const injectPCStyles = () => {
|
|
|
77
77
|
--pc-spacing-lg: 1.25rem;
|
|
78
78
|
--pc-spacing-xl: 1.75rem;
|
|
79
79
|
--pc-spacing-2xl: 2.5rem;
|
|
80
|
-
--pc-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', '
|
|
80
|
+
--pc-font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
|
81
81
|
--pc-font-size-xs: 0.75rem;
|
|
82
82
|
--pc-font-size-sm: 0.8125rem;
|
|
83
83
|
--pc-font-size-base: 0.9375rem;
|
|
@@ -111,6 +111,30 @@ export const injectPCStyles = () => {
|
|
|
111
111
|
document.head.appendChild(variablesStyle);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// Inject Inter font from Google Fonts so the SDK has a polished default
|
|
115
|
+
// even when the host app provides no custom typography. Hosts can opt out
|
|
116
|
+
// by overriding --pc-font-family.
|
|
117
|
+
const FONT_LINK_ID = 'redacto-privacy-center-fonts';
|
|
118
|
+
if (!document.getElementById(FONT_LINK_ID)) {
|
|
119
|
+
const preconnect1 = document.createElement('link');
|
|
120
|
+
preconnect1.rel = 'preconnect';
|
|
121
|
+
preconnect1.href = 'https://fonts.googleapis.com';
|
|
122
|
+
document.head.appendChild(preconnect1);
|
|
123
|
+
|
|
124
|
+
const preconnect2 = document.createElement('link');
|
|
125
|
+
preconnect2.rel = 'preconnect';
|
|
126
|
+
preconnect2.href = 'https://fonts.gstatic.com';
|
|
127
|
+
preconnect2.crossOrigin = 'anonymous';
|
|
128
|
+
document.head.appendChild(preconnect2);
|
|
129
|
+
|
|
130
|
+
const fontLink = document.createElement('link');
|
|
131
|
+
fontLink.id = FONT_LINK_ID;
|
|
132
|
+
fontLink.rel = 'stylesheet';
|
|
133
|
+
fontLink.href =
|
|
134
|
+
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap';
|
|
135
|
+
document.head.appendChild(fontLink);
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
style.textContent = `
|
|
115
139
|
/* Privacy Center SDK Styles */
|
|
116
140
|
|
|
@@ -315,6 +339,17 @@ export const injectPCStyles = () => {
|
|
|
315
339
|
.pc-select-dropdown {
|
|
316
340
|
animation: pc-slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
317
341
|
}
|
|
342
|
+
|
|
343
|
+
/* Popover Animation - matches modal/dropdown motion vocabulary */
|
|
344
|
+
.pc-popover {
|
|
345
|
+
animation: pc-fadeIn 120ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@media (prefers-reduced-motion: reduce) {
|
|
349
|
+
.pc-popover {
|
|
350
|
+
animation: none !important;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
318
353
|
|
|
319
354
|
/* Responsive dropdown - allow width to be set via inline styles */
|
|
320
355
|
.pc-select-dropdown {
|
|
@@ -2299,7 +2334,7 @@ export const injectPCStyles = () => {
|
|
|
2299
2334
|
border: 1px solid var(--pc-border-color);
|
|
2300
2335
|
background: var(--pc-bg-primary);
|
|
2301
2336
|
cursor: pointer;
|
|
2302
|
-
border-radius:
|
|
2337
|
+
border-radius: 9999px;
|
|
2303
2338
|
color: var(--pc-text-primary);
|
|
2304
2339
|
transition: color 0.3s ease, background-color 0.15s ease, border-color 0.15s ease;
|
|
2305
2340
|
padding: 0;
|
|
@@ -2458,12 +2493,14 @@ export const injectPCStyles = () => {
|
|
|
2458
2493
|
position: relative;
|
|
2459
2494
|
}
|
|
2460
2495
|
|
|
2461
|
-
|
|
2462
|
-
|
|
2496
|
+
@media (min-width: 640px) {
|
|
2497
|
+
.pc-notification-popover-offset {
|
|
2498
|
+
transform: translateX(-65px) !important;
|
|
2499
|
+
}
|
|
2463
2500
|
}
|
|
2464
2501
|
|
|
2465
2502
|
.pc-notification-dropdown {
|
|
2466
|
-
width: 380px;
|
|
2503
|
+
width: min(380px, calc(100vw - 16px));
|
|
2467
2504
|
max-height: 480px;
|
|
2468
2505
|
display: flex;
|
|
2469
2506
|
flex-direction: column;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
useState,
|
|
5
|
+
useRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { pcStyles } from '../styles/pcStyles';
|
|
3
10
|
|
|
11
|
+
// useLayoutEffect warns under SSR; fall back to useEffect when window is absent.
|
|
12
|
+
const useIsomorphicLayoutEffect =
|
|
13
|
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
14
|
+
|
|
4
15
|
export interface PCPopoverProps {
|
|
5
16
|
content: ReactNode;
|
|
6
17
|
children: ReactNode;
|
|
@@ -88,7 +99,7 @@ export function PCPopover({
|
|
|
88
99
|
setPosition({ top, left });
|
|
89
100
|
};
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
useIsomorphicLayoutEffect(() => {
|
|
92
103
|
if (isOpen) {
|
|
93
104
|
updatePosition();
|
|
94
105
|
window.addEventListener('scroll', updatePosition, true);
|