@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.
Files changed (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/chunk-6A665OPR.mjs +30 -0
  3. package/dist/index.mjs +85 -108
  4. package/dist/privacy-center.js +3517 -3281
  5. package/dist/privacy-center.mjs +3519 -3295
  6. package/package.json +1 -1
  7. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +3 -3
  8. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +204 -0
  9. package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +218 -0
  10. package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +205 -0
  11. package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +107 -0
  12. package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +100 -0
  13. package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +338 -0
  14. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +222 -0
  15. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +28 -0
  16. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +40 -0
  17. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +62 -0
  18. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +5 -27
  19. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -19
  20. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +42 -5
  21. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +13 -2
  22. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +0 -350
  23. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +0 -489
  24. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +0 -408
  25. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +0 -1
  26. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +0 -38
@@ -0,0 +1,107 @@
1
+ import { type CSSProperties } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useInternalNavigation } from '../../context/NavigationContext';
4
+ import { usePageChrome } from '../../context/PageChromeContext';
5
+ import { PAGE_CHROME_BY_PAGE } from '../../lib/constants';
6
+ import { getVar } from '../../lib/utils';
7
+
8
+ export function PageHeader() {
9
+ const { t } = useTranslation();
10
+ const { currentPage } = useInternalNavigation();
11
+ const { pageChromeOverride, headerStatus } = usePageChrome();
12
+ const pageChrome = pageChromeOverride ?? PAGE_CHROME_BY_PAGE[currentPage];
13
+
14
+ if (!pageChrome) return null;
15
+
16
+ const title = t(pageChrome.titleKey);
17
+ const description = pageChrome.descriptionKey ? t(pageChrome.descriptionKey) : null;
18
+
19
+ const wrapperStyle: CSSProperties = {
20
+ display: 'flex',
21
+ flexDirection: 'column',
22
+ gap: 4,
23
+ padding: 'var(--pc-shell-pageheader-padding, 20px 0 16px)',
24
+ borderBottom: `1px solid ${getVar('--pc-border-color')}`,
25
+ marginBottom: 16,
26
+ };
27
+
28
+ const titleRowStyle: CSSProperties = {
29
+ display: 'flex',
30
+ alignItems: 'center',
31
+ gap: 10,
32
+ minWidth: 0,
33
+ };
34
+
35
+ const titleStyle: CSSProperties = {
36
+ margin: 0,
37
+ color: getVar('--pc-text-primary'),
38
+ fontSize: 18,
39
+ fontWeight: 700,
40
+ lineHeight: 1.25,
41
+ overflow: 'hidden',
42
+ textOverflow: 'ellipsis',
43
+ whiteSpace: 'nowrap',
44
+ minWidth: 0,
45
+ };
46
+
47
+ const descriptionStyle: CSSProperties = {
48
+ margin: 0,
49
+ color: getVar('--pc-text-secondary'),
50
+ fontSize: 13,
51
+ lineHeight: 1.4,
52
+ overflow: 'hidden',
53
+ textOverflow: 'ellipsis',
54
+ whiteSpace: 'nowrap',
55
+ };
56
+
57
+ return (
58
+ <div className="pc-shell-page-header" style={wrapperStyle}>
59
+ <div style={titleRowStyle}>
60
+ <h1 style={titleStyle}>{title}</h1>
61
+ {headerStatus && (
62
+ <span
63
+ style={{
64
+ display: 'inline-flex',
65
+ alignItems: 'center',
66
+ flexShrink: 0,
67
+ borderRadius: 9999,
68
+ gap: 6,
69
+ padding: '4px 10px',
70
+ fontSize: 11,
71
+ fontWeight: 600,
72
+ lineHeight: 1.2,
73
+ border: `1px solid ${
74
+ headerStatus.variant === 'success'
75
+ ? 'rgba(16, 185, 129, 0.35)'
76
+ : getVar('--pc-warning-border')
77
+ }`,
78
+ background:
79
+ headerStatus.variant === 'success'
80
+ ? 'rgba(16, 185, 129, 0.10)'
81
+ : getVar('--pc-warning-bg'),
82
+ color:
83
+ headerStatus.variant === 'success'
84
+ ? getVar('--pc-success')
85
+ : getVar('--pc-warning-text'),
86
+ }}
87
+ >
88
+ <span
89
+ aria-hidden="true"
90
+ style={{
91
+ width: 7,
92
+ height: 7,
93
+ borderRadius: 9999,
94
+ background:
95
+ headerStatus.variant === 'success'
96
+ ? getVar('--pc-success')
97
+ : getVar('--pc-warning'),
98
+ }}
99
+ />
100
+ {headerStatus.label}
101
+ </span>
102
+ )}
103
+ </div>
104
+ {description && <p style={descriptionStyle}>{description}</p>}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,100 @@
1
+ import { useMemo, type CSSProperties } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
4
+ import { PCActionIcon } from '../../ui';
5
+ import { useMenuItemsPC } from '../../lib/constants';
6
+ import { getVar } from '../../lib/utils';
7
+ import { NavItem } from './NavItem';
8
+
9
+ type SidebarNavProps = {
10
+ isExpanded: boolean;
11
+ onToggle: () => void;
12
+ className?: string;
13
+ };
14
+
15
+ const SIDEBAR_WIDTH_EXPANDED = 240;
16
+ const SIDEBAR_WIDTH_COLLAPSED = 64;
17
+ const TOP_BAR_HEIGHT = 56;
18
+
19
+ export function SidebarNav({ isExpanded, onToggle, className }: SidebarNavProps) {
20
+ const { t } = useTranslation();
21
+ const menuItems = useMenuItemsPC();
22
+ const mode = isExpanded ? 'expanded' : 'collapsed';
23
+
24
+ const asideStyle = useMemo<CSSProperties>(
25
+ () => ({
26
+ position: 'sticky',
27
+ top: TOP_BAR_HEIGHT,
28
+ alignSelf: 'flex-start',
29
+ flexShrink: 0,
30
+ height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
31
+ width: isExpanded ? SIDEBAR_WIDTH_EXPANDED : SIDEBAR_WIDTH_COLLAPSED,
32
+ background: getVar('--pc-bg-shell'),
33
+ borderRight: `1px solid ${getVar('--pc-border-color')}`,
34
+ overflow: 'visible',
35
+ zIndex: 5,
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ }),
39
+ [isExpanded],
40
+ );
41
+
42
+ const flatNavItems = useMemo(
43
+ () => menuItems.filter((item) => Boolean(item.href)),
44
+ [menuItems],
45
+ );
46
+
47
+ return (
48
+ <aside
49
+ className={['pc-shell-sidebar', className].filter(Boolean).join(' ')}
50
+ data-expanded={isExpanded ? 'true' : 'false'}
51
+ style={asideStyle}
52
+ >
53
+ <PCActionIcon
54
+ variant="outline"
55
+ className="pc-shell-sidebar-toggle"
56
+ aria-label={isExpanded ? t('collapseSidebar') : t('expandSidebar')}
57
+ aria-expanded={isExpanded}
58
+ aria-controls="pc-shell-sidebar-nav"
59
+ onClick={onToggle}
60
+ style={{
61
+ position: 'absolute',
62
+ top: 12,
63
+ right: -14,
64
+ width: 28,
65
+ height: 28,
66
+ borderRadius: 9999,
67
+ background: getVar('--pc-bg-shell'),
68
+ borderColor: getVar('--pc-border-color'),
69
+ color: getVar('--pc-text-secondary'),
70
+ boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)',
71
+ zIndex: 10,
72
+ padding: 0,
73
+ }}
74
+ >
75
+ {isExpanded ? <PanelLeftClose size={14} /> : <PanelLeftOpen size={14} />}
76
+ </PCActionIcon>
77
+
78
+ <nav
79
+ id="pc-shell-sidebar-nav"
80
+ role="navigation"
81
+ aria-label="Privacy center primary navigation"
82
+ style={{
83
+ flex: 1,
84
+ minHeight: 0,
85
+ overflowY: 'auto',
86
+ overflowX: 'hidden',
87
+ padding: '24px 12px',
88
+ display: 'flex',
89
+ flexDirection: 'column',
90
+ gap: 4,
91
+ boxSizing: 'border-box',
92
+ }}
93
+ >
94
+ {flatNavItems.map((item) => (
95
+ <NavItem key={item.href ?? item.name} item={item} mode={mode} />
96
+ ))}
97
+ </nav>
98
+ </aside>
99
+ );
100
+ }
@@ -0,0 +1,338 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type ForwardedRef,
9
+ type Ref,
10
+ } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { Check, ChevronDown, Globe2, Menu } from 'lucide-react';
13
+ import { PCActionIcon, PCAvatar, PCSelect } from '../../ui';
14
+ import { useBranding } from '../../context/BrandingContext';
15
+ import { useOrganization } from '../../context/OrganizationContext';
16
+ import { useInternalNavigation } from '../../context/NavigationContext';
17
+ import { getVar } from '../../lib/utils';
18
+ import { languageOptions } from '../../lib/constants';
19
+ import i18n from '../../lib/i18n';
20
+ import { NotificationBell } from '../NotificationHub/NotificationBell';
21
+ import { UserMenu } from './UserMenu';
22
+
23
+ type TopAppBarProps = {
24
+ onOpenMobileNav: () => void;
25
+ showHamburger: boolean;
26
+ locale: string;
27
+ onLocaleChange: (next: string) => void;
28
+ onBack?: 'back' | 'signout';
29
+ };
30
+
31
+ export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function TopAppBar(
32
+ { onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack },
33
+ hamburgerRef,
34
+ ) {
35
+ const { t } = useTranslation();
36
+ const branding = useBranding();
37
+ const { organization } = useOrganization();
38
+ const { navigateTo } = useInternalNavigation();
39
+
40
+ const displayName = branding?.brand_name || organization?.name || '';
41
+ const hasLogoImage = Boolean(branding?.logo_url);
42
+
43
+ const handleLanguageChange = useCallback(
44
+ (value: unknown) => {
45
+ const langValue = value as { value: string; label: string } | null;
46
+ if (!langValue) return;
47
+ void i18n.changeLanguage(langValue.value);
48
+ onLocaleChange(langValue.value);
49
+ },
50
+ [onLocaleChange],
51
+ );
52
+
53
+ const headerStyle: CSSProperties = {
54
+ position: 'sticky',
55
+ top: 0,
56
+ left: 0,
57
+ right: 0,
58
+ zIndex: 50,
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ justifyContent: 'space-between',
62
+ gap: 16,
63
+ height: 56,
64
+ paddingLeft: 16,
65
+ paddingRight: 16,
66
+ background: getVar('--pc-bg-shell'),
67
+ borderBottom: `1px solid ${getVar('--pc-border-color')}`,
68
+ boxSizing: 'border-box',
69
+ };
70
+
71
+ const brandButtonStyle: CSSProperties = {
72
+ display: 'inline-flex',
73
+ alignItems: 'center',
74
+ padding: 4,
75
+ margin: 0,
76
+ border: 'none',
77
+ background: 'transparent',
78
+ color: getVar('--pc-text-primary'),
79
+ cursor: 'pointer',
80
+ borderRadius: 8,
81
+ fontFamily: 'inherit',
82
+ flexShrink: 0,
83
+ };
84
+
85
+ return (
86
+ <>
87
+ <header role="banner" className="pc-shell-topbar" style={headerStyle}>
88
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
89
+ {showHamburger && (
90
+ <HamburgerButton
91
+ ref={hamburgerRef as Ref<HTMLButtonElement>}
92
+ onClick={onOpenMobileNav}
93
+ label={t('openSidebarMenu')}
94
+ />
95
+ )}
96
+ <button
97
+ type="button"
98
+ onClick={() => navigateTo('consent-manager')}
99
+ aria-label={displayName ? `Go to ${displayName} privacy center` : 'Go to privacy center'}
100
+ style={brandButtonStyle}
101
+ onMouseEnter={(e) => {
102
+ e.currentTarget.style.background = getVar('--pc-bg-hover');
103
+ }}
104
+ onMouseLeave={(e) => {
105
+ e.currentTarget.style.background = 'transparent';
106
+ }}
107
+ >
108
+ <span
109
+ style={{
110
+ display: 'inline-flex',
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ flexShrink: 0,
114
+ height: 32,
115
+ }}
116
+ >
117
+ {hasLogoImage ? (
118
+ <img
119
+ src={branding!.logo_url!}
120
+ alt={displayName}
121
+ style={{
122
+ height: 32,
123
+ width: 'auto',
124
+ maxWidth: 200,
125
+ objectFit: 'contain',
126
+ display: 'block',
127
+ }}
128
+ />
129
+ ) : branding?.icon_url ? (
130
+ <PCAvatar src={branding.icon_url} name={displayName} size="md" color="info" />
131
+ ) : (
132
+ <PCAvatar name={displayName} size="md" color="info" />
133
+ )}
134
+ </span>
135
+ </button>
136
+ </div>
137
+
138
+ <div
139
+ style={{
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ gap: 8,
143
+ flexShrink: 0,
144
+ }}
145
+ className="pc-shell-topbar-actions"
146
+ >
147
+ {showHamburger ? (
148
+ <CompactLanguageMenu
149
+ locale={locale}
150
+ onChange={(value) => {
151
+ void i18n.changeLanguage(value);
152
+ onLocaleChange(value);
153
+ }}
154
+ />
155
+ ) : (
156
+ <PCSelect
157
+ size="sm"
158
+ searchable
159
+ prefix={<Globe2 size={12} strokeWidth={1.8} />}
160
+ searchPlaceholder={t('searchPlaceholder')}
161
+ noOptionsMessage={t('noOptionsFound')}
162
+ style={{
163
+ width: 'auto',
164
+ minWidth: 120,
165
+ maxWidth: 'min(60vw, 280px)',
166
+ flexShrink: 0,
167
+ }}
168
+ options={languageOptions}
169
+ value={languageOptions.find((opt) => opt.value === locale) || null}
170
+ onChange={handleLanguageChange}
171
+ />
172
+ )}
173
+ <NotificationBell />
174
+ <UserMenu onBack={onBack} />
175
+ </div>
176
+ </header>
177
+ <style>{`
178
+ @media (max-width: 639px) {
179
+ .pc-shell-topbar { padding-left: 8px !important; padding-right: 8px !important; gap: 8px !important; }
180
+ .pc-shell-topbar-actions { gap: 4px !important; }
181
+ }
182
+ `}</style>
183
+ </>
184
+ );
185
+ });
186
+
187
+ type CompactLanguageMenuProps = {
188
+ locale: string;
189
+ onChange: (value: string) => void;
190
+ };
191
+
192
+ function CompactLanguageMenu({ locale, onChange }: CompactLanguageMenuProps) {
193
+ const containerRef = useRef<HTMLDivElement>(null);
194
+ const [open, setOpen] = useState(false);
195
+
196
+ useEffect(() => {
197
+ if (!open) return;
198
+ const onDocMouseDown = (event: MouseEvent) => {
199
+ if (!containerRef.current?.contains(event.target as Node)) {
200
+ setOpen(false);
201
+ }
202
+ };
203
+ const onKey = (event: KeyboardEvent) => {
204
+ if (event.key === 'Escape') setOpen(false);
205
+ };
206
+ document.addEventListener('mousedown', onDocMouseDown);
207
+ document.addEventListener('keydown', onKey);
208
+ return () => {
209
+ document.removeEventListener('mousedown', onDocMouseDown);
210
+ document.removeEventListener('keydown', onKey);
211
+ };
212
+ }, [open]);
213
+
214
+ const code = (locale || 'en').split('-')[0]!.toUpperCase();
215
+ const selectedLabel =
216
+ languageOptions.find((opt) => opt.value === locale)?.label ?? locale;
217
+
218
+ return (
219
+ <div ref={containerRef} style={{ position: 'relative', flexShrink: 0 }}>
220
+ <button
221
+ type="button"
222
+ aria-haspopup="listbox"
223
+ aria-expanded={open}
224
+ aria-label={`Language: ${selectedLabel}`}
225
+ onClick={() => setOpen((v) => !v)}
226
+ style={{
227
+ display: 'inline-flex',
228
+ alignItems: 'center',
229
+ gap: 4,
230
+ height: 32,
231
+ padding: '0 8px 0 10px',
232
+ borderRadius: 9999,
233
+ background: getVar('--pc-bg-shell'),
234
+ border: `1px solid ${getVar('--pc-border-color')}`,
235
+ color: getVar('--pc-text-primary'),
236
+ cursor: 'pointer',
237
+ fontFamily: 'inherit',
238
+ fontSize: 12,
239
+ fontWeight: 600,
240
+ letterSpacing: '0.02em',
241
+ lineHeight: 1,
242
+ }}
243
+ >
244
+ {code}
245
+ <ChevronDown size={12} strokeWidth={2} />
246
+ </button>
247
+ {open && (
248
+ <div
249
+ role="listbox"
250
+ aria-label="Language"
251
+ style={{
252
+ position: 'absolute',
253
+ top: 'calc(100% + 6px)',
254
+ right: 0,
255
+ minWidth: 200,
256
+ maxHeight: 320,
257
+ overflowY: 'auto',
258
+ background: getVar('--pc-bg-primary'),
259
+ border: `1px solid ${getVar('--pc-border-color')}`,
260
+ borderRadius: 12,
261
+ boxShadow: '0 8px 24px rgba(15, 23, 42, 0.15)',
262
+ padding: 6,
263
+ zIndex: 1060,
264
+ }}
265
+ >
266
+ {languageOptions.map((opt) => {
267
+ const selected = opt.value === locale;
268
+ return (
269
+ <button
270
+ key={opt.value}
271
+ type="button"
272
+ role="option"
273
+ aria-selected={selected}
274
+ onClick={() => {
275
+ onChange(opt.value);
276
+ setOpen(false);
277
+ }}
278
+ onMouseEnter={(e) => {
279
+ if (!selected) {
280
+ e.currentTarget.style.background = getVar('--pc-bg-hover');
281
+ }
282
+ }}
283
+ onMouseLeave={(e) => {
284
+ if (!selected) {
285
+ e.currentTarget.style.background = 'transparent';
286
+ }
287
+ }}
288
+ style={{
289
+ display: 'flex',
290
+ alignItems: 'center',
291
+ justifyContent: 'space-between',
292
+ width: '100%',
293
+ padding: '8px 10px',
294
+ background: selected ? getVar('--pc-bg-hover') : 'transparent',
295
+ color: selected
296
+ ? getVar('--pc-primary')
297
+ : getVar('--pc-text-primary'),
298
+ border: 'none',
299
+ borderRadius: 8,
300
+ fontFamily: 'inherit',
301
+ fontSize: 13,
302
+ fontWeight: selected ? 600 : 500,
303
+ cursor: 'pointer',
304
+ textAlign: 'left',
305
+ transition: 'background 150ms ease',
306
+ }}
307
+ >
308
+ <span>{opt.label}</span>
309
+ {selected && <Check size={14} strokeWidth={2} />}
310
+ </button>
311
+ );
312
+ })}
313
+ </div>
314
+ )}
315
+ </div>
316
+ );
317
+ }
318
+
319
+ const HamburgerButton = forwardRef<HTMLButtonElement, { onClick: () => void; label: string }>(
320
+ function HamburgerButton({ onClick, label }, ref: ForwardedRef<HTMLButtonElement>) {
321
+ return (
322
+ <PCActionIcon
323
+ ref={ref}
324
+ variant="default"
325
+ aria-label={label}
326
+ onClick={onClick}
327
+ style={{
328
+ width: 40,
329
+ height: 40,
330
+ padding: 0,
331
+ color: getVar('--pc-text-primary'),
332
+ }}
333
+ >
334
+ <Menu size={20} />
335
+ </PCActionIcon>
336
+ );
337
+ },
338
+ );