@redacto.io/consent-sdk-react 10.0.0-beta.3 → 10.0.0-beta.4

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": "10.0.0-beta.3",
3
+ "version": "10.0.0-beta.4",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1248,60 +1248,6 @@ const as: StaticStrings = {
1248
1248
  close: "বন্ধ কৰক",
1249
1249
  };
1250
1250
 
1251
- const bho: StaticStrings = {
1252
- useEmailInstead: "ईमेल इस्तेमाल करीं",
1253
- useMobileInstead: "मोबाइल इस्तेमाल करीं",
1254
- emailAddress: "ईमेल पता",
1255
- otpHelperEmail: "हमनी एह ईमेल पर एक बेर इस्तेमाल होखे वाला पासवर्ड भेजब।",
1256
- changeEmail: "ईमेल बदलीं",
1257
- expandDetails: "{name} के विवरण फैलाईं",
1258
- collapseDetails: "{name} के विवरण समेटीं",
1259
- selectAllFor: "{name} खातिर सभ डेटा तत्व चुनीं",
1260
- selectElement: "{name} चुनीं",
1261
- selectElementRequired: "{name} चुनीं (जरूरी)",
1262
- required: "जरूरी",
1263
- logoAlt: "लोगो",
1264
- selectLanguage: "भाषा चुनीं",
1265
- selectLanguageCurrent: "भाषा चुनीं. मौजूदा भाषा: {lang}",
1266
- opensInNewTab: "नया टैब में खुलेला",
1267
- audioStart: "ऑडियो प्लेबैक शुरू करीं",
1268
- audioPause: "ऑडियो प्लेबैक रोकीं",
1269
- talkbackStart: "टॉकबैक शुरू करीं",
1270
- talkbackPause: "टॉकबैक रोकीं",
1271
- mobileNumber: "मोबाइल नंबर",
1272
- otpHelper: "हमनी एह नंबर पर एक बेर इस्तेमाल होखे वाला पासवर्ड भेजब।",
1273
- otpSentTo: "ओटीपी (OTP) भेजल गइल",
1274
- changeMobile: "मोबाइल नंबर बदलीं",
1275
- enterNDigitOtp: "{count}-अंक के ओटीपी (OTP) डालीं",
1276
- otpDigit: "ओटीपी (OTP) अंक {index}",
1277
- resendIn: "{seconds} सेकंड में ओटीपी (OTP) फेर से भेजीं",
1278
- sending: "भेजल जा रहल बा…",
1279
- resendOtp: "ओटीपी (OTP) फेर से भेजीं",
1280
- getOtp: "ओटीपी (OTP) पाईं",
1281
- verifying: "सत्यापित कइल जा रहल बा…",
1282
- verifyOtp: "ओटीपी (OTP) सत्यापित करीं",
1283
- sendOtp: "ओटीपी (OTP) भेजीं",
1284
- confirmOtp: "ओटीपी (OTP) के पुष्टि करीं",
1285
- verifyWithOtp: "ओटीपी (OTP) से सत्यापित करीं",
1286
- editSelections: "चुनाव में बदलाव करीं",
1287
- otpIncorrect:
1288
- "रउरा डालल सत्यापन कोड गलत बा। कृपया जाँच करीं आ फेर से कोशिश करीं।",
1289
- otpExpired: "रउरा सत्यापन कोड के मियाद खतम हो गइल बा। कृपया नया कोड मांगीं।",
1290
- otpNotFound: "सत्यापन कोड ना मिलल। कृपया नया कोड मांगीं।",
1291
- otpVerifyFailed: "ओटीपी (OTP) के सत्यापन ना हो सकल। कृपया फेर से कोशिश करीं।",
1292
- otpSendFailed: "ओटीपी (OTP) ना भेजल जा सकल। कृपया फेर से कोशिश करीं।",
1293
- consentConfirmed: "सहमति के पुष्टि भइल",
1294
- recordedUnder: "ओटीपी (OTP) सत्यापित · {regulation} के तहत दर्ज भइल",
1295
- recordedUnderAt:
1296
- "ओटीपी (OTP) सत्यापित · {regulation} के तहत {time} बजे दर्ज भइल",
1297
- consentReferenceId: "सहमति संदर्भ आईडी",
1298
- proceed: "आगे बढ़ीं",
1299
- loadingNotice: "नोटिस लोड हो रहल बा",
1300
- loadFailedTitle: "सहमति नोटिस लोड ना हो सकल",
1301
- tryAgain: "कृपया फेर से कोसिस करीं।",
1302
- close: "बंद करीं",
1303
- };
1304
-
1305
1251
  const brx: StaticStrings = {
1306
1252
  useEmailInstead: "इमेल बाहाय",
1307
1253
  useMobileInstead: "मबाइल बाहाय",
@@ -1435,7 +1381,6 @@ export const STATIC_STRINGS: Record<string, StaticStrings> = {
1435
1381
  te,
1436
1382
  ur,
1437
1383
  as,
1438
- bho,
1439
1384
  brx,
1440
1385
  doi,
1441
1386
  };
@@ -6,7 +6,6 @@ export const LANGUAGE_NATIVE_LABELS: Record<string, string> = {
6
6
  English: "English",
7
7
  Assamese: "অসমীয়া (Assamese)",
8
8
  Bengali: "বাংলা (Bengali)",
9
- Bhojpuri: "भोजपुरी (Bhojpuri)",
10
9
  Bodo: "बड़ो (Bodo)",
11
10
  Dogri: "डोगरी (Dogri)",
12
11
  Gujarati: "ગુજરાતી (Gujarati)",
@@ -44,7 +43,6 @@ const LANGUAGE_NAME_TO_BCP47: Record<string, string> = {
44
43
  English: "en",
45
44
  Assamese: "as",
46
45
  Bengali: "bn",
47
- Bhojpuri: "bho",
48
46
  Bodo: "brx",
49
47
  Dogri: "doi",
50
48
  Gujarati: "gu",
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState, type CSSProperties } from 'react';
2
- import { ChevronDown, Users } from 'lucide-react';
2
+ import { Users } from 'lucide-react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { PCAvatar, PCPopover } from '../../ui';
5
5
  import { useBranding } from '../../context/BrandingContext';
@@ -56,17 +56,11 @@ export function UserMenu({ onBack }: UserMenuProps) {
56
56
  const displayName = branding?.brand_name || organization?.name || '';
57
57
  const triggerAvatarName = email ? initialsFromEmail(email) : displayName || '?';
58
58
 
59
- const triggerStyle: CSSProperties = {
60
- display: 'inline-flex',
61
- alignItems: 'center',
62
- gap: 6,
63
- padding: '4px 6px 4px 4px',
64
- borderRadius: 9999,
65
- background: 'transparent',
66
- border: `1px solid ${getVar('--pc-border-color')}`,
67
- color: getVar('--pc-text-secondary'),
68
- cursor: 'pointer',
69
- transition: 'background 150ms ease, border-color 150ms ease, box-shadow 150ms ease',
59
+ // The .pc-navbar-avatar-btn control draws the ring and surface, so the avatar fills
60
+ // it and draws neither (see `framed={false}` below).
61
+ const avatarFillStyle: CSSProperties = {
62
+ width: '100%',
63
+ height: '100%',
70
64
  };
71
65
 
72
66
  // Shared style for the popover's action rows (switch profile, back/sign out).
@@ -89,24 +83,25 @@ export function UserMenu({ onBack }: UserMenuProps) {
89
83
  };
90
84
 
91
85
  const trigger = (
92
- <div
93
- style={triggerStyle}
94
- role="presentation"
95
- onMouseEnter={(e) => {
96
- (e.currentTarget as HTMLDivElement).style.boxShadow =
97
- '0 0 0 2px color-mix(in srgb, var(--pc-primary) 18%, transparent)';
98
- }}
99
- onMouseLeave={(e) => {
100
- (e.currentTarget as HTMLDivElement).style.boxShadow = 'none';
101
- }}
102
- >
86
+ <button type="button" aria-label={t('accountMenu')} className="pc-navbar-avatar-btn">
103
87
  {branding?.icon_url ? (
104
- <PCAvatar src={branding.icon_url} name={triggerAvatarName} size="sm" color="info" />
88
+ <PCAvatar
89
+ src={branding.icon_url}
90
+ name={triggerAvatarName}
91
+ size="md"
92
+ color="info"
93
+ framed={false}
94
+ style={avatarFillStyle}
95
+ />
105
96
  ) : (
106
- <PCAvatar name={triggerAvatarName} size="sm" color="info" />
97
+ <PCAvatar
98
+ name={triggerAvatarName}
99
+ size="md"
100
+ color="info"
101
+ style={avatarFillStyle}
102
+ />
107
103
  )}
108
- <ChevronDown size={14} strokeWidth={2} />
109
- </div>
104
+ </button>
110
105
  );
111
106
 
112
107
  const content = (
@@ -40,7 +40,6 @@ export const languageOptions = [
40
40
  { value: 'mni-Mtei', label: 'ꯃꯩꯇꯩꯂꯣꯟ (Manipuri)' },
41
41
  { value: 'gom', label: 'कोंकणी (Goan Konkani)' },
42
42
  { value: 'sa', label: 'संस्कृतम् (Sanskrit)' },
43
- { value: 'bho', label: 'भोजपुरी (Bhojpuri)' },
44
43
  { value: 'brx', label: 'बड़ो (Bodo)' },
45
44
  { value: 'ks', label: 'كٲشُر (Kashmiri)' },
46
45
  { value: 'sat', label: 'ᱥᱟᱱᱛᱟᱲᱤ (Santali)' },
@@ -2,7 +2,6 @@
2
2
  import enTranslation from '../locales/en/translation.json';
3
3
  import asTranslation from '../locales/as/translation.json';
4
4
  import bnTranslation from '../locales/bn/translation.json';
5
- import bhoTranslation from '../locales/bho/translation.json';
6
5
  import brxTranslation from '../locales/brx/translation.json';
7
6
  import doiTranslation from '../locales/doi/translation.json';
8
7
  import gomTranslation from '../locales/gom/translation.json';
@@ -28,7 +27,6 @@ const activityFetchErrorTranslations: Record<string, string> = {
28
27
  en: 'Failed to fetch activity data',
29
28
  as: 'কাৰ্যকলাপৰ তথ্য আনিবলৈ ব্যৰ্থ',
30
29
  bn: 'কার্যকলাপের ডেটা আনতে ব্যর্থ',
31
- bho: 'गतिविधि डेटा लावे में विफल',
32
30
  brx: 'खामानि डाटा लाङो दिहुनायाव असफल',
33
31
  doi: 'गतिविधि डेटा लावै मे असफल',
34
32
  gom: 'क्रियाकलाप डेटा आणपांत अयशस्वी',
@@ -68,7 +66,6 @@ const translations: Record<string, Record<string, any>> = {
68
66
  en: { translation: withSharedActivityErrors('en', enTranslation) },
69
67
  as: { translation: withSharedActivityErrors('as', asTranslation) },
70
68
  bn: { translation: withSharedActivityErrors('bn', bnTranslation) },
71
- bho: { translation: withSharedActivityErrors('bho', bhoTranslation) },
72
69
  brx: { translation: withSharedActivityErrors('brx', brxTranslation) },
73
70
  doi: { translation: withSharedActivityErrors('doi', doiTranslation) },
74
71
  gom: { translation: withSharedActivityErrors('gom', gomTranslation) },
@@ -353,5 +353,6 @@
353
353
  "couldNotSwitchProfile": "Could not switch to that profile. Please try again.",
354
354
  "purposeNotRevocable": "This purpose can't be withdrawn directly.",
355
355
  "purposeNotRevocableError": "This purpose can't be withdrawn directly. Please raise a request to remove it.",
356
- "userIdLabel": "User ID: {{id}}"
356
+ "userIdLabel": "User ID: {{id}}",
357
+ "accountMenu": "Account menu"
357
358
  }
@@ -2755,7 +2755,10 @@ export const injectPCStyles = () => {
2755
2755
  border-radius: 9999px;
2756
2756
  }
2757
2757
 
2758
- .pc-notification-bell-btn {
2758
+ /* Circular navbar controls: the bell and the org-logo menu button. Same size
2759
+ token, ring and hover, so the topbar cluster reads as one set of controls. */
2760
+ .pc-notification-bell-btn,
2761
+ .pc-navbar-avatar-btn {
2759
2762
  display: flex;
2760
2763
  align-items: center;
2761
2764
  justify-content: center;
@@ -2777,7 +2780,8 @@ export const injectPCStyles = () => {
2777
2780
  height: 14px;
2778
2781
  }
2779
2782
 
2780
- .pc-notification-bell-btn:hover {
2783
+ .pc-notification-bell-btn:hover,
2784
+ .pc-navbar-avatar-btn:hover {
2781
2785
  background-color: var(--pc-bg-hover);
2782
2786
  border-color: var(--pc-border-color-hover);
2783
2787
  }
@@ -2840,6 +2844,7 @@ export const injectPCStyles = () => {
2840
2844
  /* Keyboard focus rings for Notification Hub interactive controls */
2841
2845
  .pc-notification-cta:focus-visible,
2842
2846
  .pc-notification-bell-btn:focus-visible:not(:disabled),
2847
+ .pc-navbar-avatar-btn:focus-visible:not(:disabled),
2843
2848
  .pc-notification-filter-tab:focus-visible:not(:disabled),
2844
2849
  .pc-notification-pagination-btn:focus-visible:not(:disabled),
2845
2850
  .pc-notification-link-btn:focus-visible:not(:disabled),
@@ -669,6 +669,23 @@ export const pcStyles = {
669
669
  height: '48px',
670
670
  fontSize: getVar('--pc-font-size-base'),
671
671
  },
672
+ // Image avatars (org logos) must NOT sit on the brand-colour fill `avatar` sets:
673
+ // --pc-info resolves to the org's own primary colour, so a brand-coloured mark on a
674
+ // transparent background would be invisible against it. Neutral surface + hairline
675
+ // border instead. border-box keeps the rendered box at the exact avatarSm/Md/Lg size.
676
+ avatarImage: {
677
+ backgroundColor: getVar('--pc-bg-shell'),
678
+ border: `1px solid ${getVar('--pc-border-color')}`,
679
+ boxSizing: 'border-box',
680
+ objectFit: 'contain',
681
+ padding: '3px',
682
+ },
683
+ // For an image avatar inside a control that already draws the ring and surface
684
+ // (the navbar avatar button) — otherwise the two rings nest as concentric circles.
685
+ avatarImageBare: {
686
+ backgroundColor: 'transparent',
687
+ border: 'none',
688
+ },
672
689
 
673
690
  // Empty state styles
674
691
  empty: {
@@ -0,0 +1,83 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
3
+ import { PCAvatar } from './PCAvatar';
4
+
5
+ /**
6
+ * Regression: PCAvatar applied a single style object to BOTH branches, so an image
7
+ * avatar (the org logo) was painted onto a disc filled with var(--pc-info) — which
8
+ * applyBrandingTheme maps to the org's own primary colour. A brand-coloured mark on a
9
+ * transparent background therefore rendered as a featureless brand-coloured circle.
10
+ */
11
+
12
+ const LOGO_SRC = 'https://cdn.example.com/org-icon.svg';
13
+
14
+ afterEach(cleanup);
15
+
16
+ describe('PCAvatar', () => {
17
+ it('does not paint the brand-colour fill behind a logo image', () => {
18
+ render(<PCAvatar src={LOGO_SRC} name="Acme Corp" color="info" />);
19
+
20
+ const image = screen.getByRole('img');
21
+ expect(image.style.backgroundColor).not.toBe('var(--pc-info)');
22
+ expect(image.style.backgroundColor).toBe('var(--pc-bg-shell)');
23
+ });
24
+
25
+ it('contains the logo instead of stretching it, and keeps the box at its nominal size', () => {
26
+ render(<PCAvatar src={LOGO_SRC} name="Acme Corp" size="sm" color="info" />);
27
+
28
+ const image = screen.getByRole('img');
29
+ expect(image.style.objectFit).toBe('contain');
30
+ expect(image.style.boxSizing).toBe('border-box');
31
+ expect(image.style.width).toBe('24px');
32
+ expect(image.style.height).toBe('24px');
33
+ });
34
+
35
+ it('falls back to initials when the logo fails to load', () => {
36
+ render(<PCAvatar src={LOGO_SRC} name="Acme Corp" color="info" />);
37
+
38
+ fireEvent.error(screen.getByRole('img'));
39
+
40
+ expect(screen.queryByRole('img')).toBeNull();
41
+ expect(screen.getByText('AC')).toBeTruthy();
42
+ });
43
+
44
+ it('still fills initials avatars with the requested colour', () => {
45
+ render(<PCAvatar name="Acme Corp" color="info" />);
46
+
47
+ expect(screen.getByText('AC').style.backgroundColor).toBe('var(--pc-info)');
48
+ });
49
+
50
+ it('keeps its tile and ring by default (standalone brand mark)', () => {
51
+ render(<PCAvatar src={LOGO_SRC} name="Acme Corp" color="info" />);
52
+
53
+ const image = screen.getByRole('img');
54
+ expect(image.style.border).toContain('var(--pc-border-color)');
55
+ expect(image.style.backgroundColor).toBe('var(--pc-bg-shell)');
56
+ });
57
+
58
+ it('drops its tile and ring when framed={false}, so it cannot nest inside a bordered control', () => {
59
+ render(<PCAvatar src={LOGO_SRC} name="Acme Corp" color="info" framed={false} />);
60
+
61
+ const image = screen.getByRole('img');
62
+ expect(image.style.border).not.toContain('--pc-border-color');
63
+ expect(image.style.backgroundColor).toBe('transparent');
64
+ // Still contained — framed only controls the chrome, not the fit.
65
+ expect(image.style.objectFit).toBe('contain');
66
+ });
67
+
68
+ it('does not strip the initials fill when framed={false}', () => {
69
+ render(<PCAvatar name="Acme Corp" color="info" framed={false} />);
70
+
71
+ expect(screen.getByText('AC').style.backgroundColor).toBe('var(--pc-info)');
72
+ });
73
+
74
+ it('lets a caller override the style on both branches', () => {
75
+ const { rerender } = render(
76
+ <PCAvatar src={LOGO_SRC} name="Acme Corp" style={{ backgroundColor: 'red' }} />,
77
+ );
78
+ expect(screen.getByRole('img').style.backgroundColor).toBe('red');
79
+
80
+ rerender(<PCAvatar name="Acme Corp" style={{ backgroundColor: 'red' }} />);
81
+ expect(screen.getByText('AC').style.backgroundColor).toBe('red');
82
+ });
83
+ });
@@ -1,14 +1,17 @@
1
- import { type CSSProperties } from 'react';
1
+ import { useEffect, useState, type CSSProperties } from 'react';
2
2
  import { pcStyles } from '../styles/pcStyles';
3
3
 
4
- export interface PCAvatarProps {
4
+ export type PCAvatarProps = Readonly<{
5
5
  name?: string;
6
6
  src?: string;
7
7
  className?: string;
8
8
  style?: CSSProperties;
9
9
  size?: 'sm' | 'md' | 'lg';
10
10
  color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info';
11
- }
11
+ // Image avatars draw their own surface + hairline ring. Set false when a parent
12
+ // control already draws them, so the two don't nest as concentric rings.
13
+ framed?: boolean;
14
+ }>;
12
15
 
13
16
  function getInitials(name: string): string {
14
17
  return name
@@ -40,29 +43,50 @@ export function PCAvatar({
40
43
  style = {},
41
44
  size = 'md',
42
45
  color = 'primary',
46
+ framed = true,
43
47
  }: PCAvatarProps) {
44
- const avatarStyle: CSSProperties = {
48
+ const [failed, setFailed] = useState(false);
49
+
50
+ // A new src deserves a fresh attempt — otherwise switching org/profile strands
51
+ // the avatar on the previous image's failure.
52
+ useEffect(() => setFailed(false), [src]);
53
+
54
+ const baseStyle: CSSProperties = {
45
55
  ...pcStyles.avatar,
46
56
  ...(size === 'sm' && pcStyles.avatarSm),
47
57
  ...(size === 'md' && pcStyles.avatarMd),
48
58
  ...(size === 'lg' && pcStyles.avatarLg),
49
- backgroundColor: getColor(color),
50
- ...style,
51
59
  };
52
60
 
53
- if (src) {
61
+ if (src && !failed) {
62
+ // avatarImage overrides the brand-colour fill baked into pcStyles.avatar; a logo
63
+ // is usually a brand-coloured mark on transparency and would vanish against it.
64
+ const imageStyle: CSSProperties = {
65
+ ...baseStyle,
66
+ ...pcStyles.avatarImage,
67
+ ...(!framed && pcStyles.avatarImageBare),
68
+ ...style,
69
+ };
70
+
54
71
  return (
55
72
  <img
56
73
  src={src}
57
74
  alt={name}
58
75
  className={className}
59
- style={avatarStyle}
76
+ style={imageStyle}
77
+ onError={() => setFailed(true)}
60
78
  />
61
79
  );
62
80
  }
63
81
 
82
+ const initialsStyle: CSSProperties = {
83
+ ...baseStyle,
84
+ backgroundColor: getColor(color),
85
+ ...style,
86
+ };
87
+
64
88
  return (
65
- <div className={className} style={avatarStyle}>
89
+ <div className={className} style={initialsStyle}>
66
90
  {name ? getInitials(name) : '?'}
67
91
  </div>
68
92
  );