kyd-shared-badge 0.3.16 → 0.3.18

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": "kyd-shared-badge",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -155,6 +155,12 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
155
155
  isPublic={true}
156
156
  badgeImageUrl={badgeData.badgeImageUrl || ''}
157
157
  summary={report_summary}
158
+ enterpriseMatch={(() => {
159
+ const em = assessmentResult?.enterprise_match;
160
+ if (!em) return null;
161
+ const role = em.role || {};
162
+ return { score: typeof em.score === 'number' ? em.score : undefined, description: em.description, roleName: role.name };
163
+ })()}
158
164
  countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
159
165
  />
160
166
  </Reveal>
package/src/colors.ts CHANGED
@@ -64,4 +64,37 @@ const red3 = '#F5B7B3'
64
64
  const red4 = '#FADBDA'
65
65
  const red5 = '#FDF0EF'
66
66
 
67
- export { red1, red2, red3, red4, red5 }
67
+ export { red1, red2, red3, red4, red5 }
68
+
69
+
70
+
71
+ export function clampPercent(value?: number): number {
72
+ const n = Math.round(Number(value ?? 0))
73
+ if (Number.isNaN(n)) return 0
74
+ return Math.max(0, Math.min(100, n))
75
+ }
76
+
77
+ export function scoreToColorHex(score: number): string {
78
+ const pct = clampPercent(score)
79
+ if (pct <= 33) return red
80
+ if (pct <= 66) return yellow
81
+ return green
82
+ }
83
+
84
+ export function scoreToCssVar(score: number): string {
85
+ const pct = clampPercent(score)
86
+ if (pct <= 33) return `var(--status-negative, ${red})`
87
+ if (pct <= 66) return `var(--status-neutral, ${yellow})`
88
+ return `var(--status-positive, ${green})`
89
+ }
90
+
91
+ export function hexToRgba(hex: string, alpha: number): string {
92
+ const clean = hex.replace('#', '')
93
+ const r = parseInt(clean.substring(0, 2), 16)
94
+ const g = parseInt(clean.substring(2, 4), 16)
95
+ const b = parseInt(clean.substring(4, 6), 16)
96
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`
97
+ }
98
+
99
+
100
+
@@ -3,23 +3,11 @@
3
3
  import React from 'react';
4
4
  import BusinessRuleLink from './BusinessRuleLink';
5
5
  import { FiInfo } from 'react-icons/fi';
6
- import { green, yellow, red } from '../colors';
6
+ import { hexToRgba, scoreToColorHex, scoreToCssVar, clampPercent } from '../colors';
7
7
 
8
8
  type TopMover = { label?: string; uid?: string };
9
9
 
10
- const hexToRgba = (hex: string, alpha: number) => {
11
- const clean = hex.replace('#', '');
12
- const r = parseInt(clean.substring(0, 2), 16);
13
- const g = parseInt(clean.substring(2, 4), 16);
14
- const b = parseInt(clean.substring(4, 6), 16);
15
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
16
- };
17
-
18
- const pickTint = (score: number) => {
19
- if (score >= 75) return green;
20
- if (score >= 50) return yellow;
21
- return red;
22
- };
10
+ // color helpers moved to ../utils/color
23
11
 
24
12
  export default function GaugeCard({
25
13
  title,
@@ -38,7 +26,7 @@ export default function GaugeCard({
38
26
  topMoversTitle?: string;
39
27
  tooltipText?: string;
40
28
  }) {
41
- const pct = Math.max(0, Math.min(100, Math.round(Number(percent ?? 0))));
29
+ const pct = clampPercent(percent);
42
30
  const displayLabel = label || '';
43
31
  // Use a fixed internal coordinate system and scale the SVG to container for responsiveness
44
32
  const size = 280;
@@ -47,13 +35,8 @@ export default function GaugeCard({
47
35
  const circumference = Math.PI * radius;
48
36
  const progress = pct / 100;
49
37
  const dash = circumference * progress;
50
- const progressColor =
51
- pct <= 33
52
- ? `var(--status-negative, ${red})`
53
- : pct <= 66
54
- ? `var(--status-neutral, ${yellow})`
55
- : `var(--status-positive, ${green})`;
56
- const headerTint = hexToRgba(pickTint(pct), 0.06);
38
+ const progressColor = scoreToCssVar(pct);
39
+ const headerTint = hexToRgba(scoreToColorHex(pct), 0.06);
57
40
 
58
41
  return (
59
42
  <div
@@ -4,6 +4,7 @@ import Image from 'next/image';
4
4
  import { formatLocalDate } from '../utils/date';
5
5
  import countriesLib from 'i18n-iso-countries';
6
6
  import enLocale from 'i18n-iso-countries/langs/en.json';
7
+ import { FiInfo } from 'react-icons/fi';
7
8
 
8
9
  // Register English locale once at module import time
9
10
  countriesLib.registerLocale(enLocale);
@@ -36,13 +37,16 @@ interface ReportHeaderProps {
36
37
  isPublic: boolean;
37
38
  badgeImageUrl: string;
38
39
  summary?: string;
40
+ enterpriseMatch?: { score?: number; description?: string; roleName?: string } | null;
39
41
  countries?: string[];
40
42
  }
41
43
 
42
- const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImageUrl, summary, countries = [] }: ReportHeaderProps) => {
44
+ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImageUrl, summary, enterpriseMatch, countries = [] }: ReportHeaderProps) => {
43
45
  // Use the dynamic image if available, otherwise fall back to the score-based one.
44
46
  const finalBadgeImageUrl = badgeImageUrl || getBadgeImageUrl(score || 0);
45
47
  const tint = hexToRgba(pickTint(score || 0), 0.06);
48
+ const matchScore = typeof enterpriseMatch?.score === 'number' ? enterpriseMatch.score : undefined;
49
+ const matchTint = matchScore !== undefined ? hexToRgba(pickTint(matchScore), 0.06) : undefined;
46
50
 
47
51
  const formattedDate = updatedAt ? formatLocalDate(updatedAt, {
48
52
  year: 'numeric',
@@ -59,7 +63,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
59
63
  {/* Left Half: Badge Image with robust centered overlay */}
60
64
  <div className="w-full md:w-1/3 flex items-center justify-center self-stretch">
61
65
  <div className="relative w-full max-w-xs select-none">
62
- <Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} unoptimized className='w-full h-auto pointer-events-none p-10'/>
66
+ <Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
63
67
  {/* Centered overlay slightly lower on Y axis, responsive and readable */}
64
68
  <div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
65
69
  <div className="font-extrabold text-black text-3xl " >
@@ -96,7 +100,32 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
96
100
  })()
97
101
  )}
98
102
  </div>
99
- {summary && (
103
+ {(enterpriseMatch?.description || enterpriseMatch?.score !== undefined) ? (
104
+ <div className={'hidden md:block text-sm space-y-2 pt-4'} style={{ borderTop: '1px solid var(--icon-button-secondary)' }}>
105
+ <div className="flex items-center justify-between">
106
+ <div className="flex items-center gap-2">
107
+ <p className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Role Match{enterpriseMatch?.roleName ? ` — ${enterpriseMatch.roleName}` : ''}</p>
108
+ <span className={'relative inline-flex items-center group'} style={{ color: 'var(--text-secondary)' }}>
109
+ <FiInfo size={16} />
110
+ <div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
111
+ <div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
112
+ <div style={{ fontWeight: 600 }}>AI-generated</div>
113
+ <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>Role match score and description are AI-generated.</div>
114
+ </div>
115
+ </div>
116
+ </span>
117
+ </div>
118
+ {typeof enterpriseMatch?.score === 'number' && (
119
+ <span className="px-2 py-1 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--content-card-background)', backgroundImage: matchTint ? `linear-gradient(${matchTint}, ${matchTint})` : undefined, border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>
120
+ {Math.round(enterpriseMatch.score)}%
121
+ </span>
122
+ )}
123
+ </div>
124
+ {enterpriseMatch?.description && (
125
+ <p className={'text-sm'} style={{ color: 'var(--text-main)' }}>{enterpriseMatch.description}</p>
126
+ )}
127
+ </div>
128
+ ) : summary && (
100
129
  <div className={'hidden md:block text-sm space-y-2 pt-4'} style={{ borderTop: '1px solid var(--icon-button-secondary)' }}>
101
130
  <div>
102
131
  <p className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Summary:</p>
@@ -3,7 +3,7 @@
3
3
  import React from 'react';
4
4
  import BusinessRuleLink from './BusinessRuleLink';
5
5
  import { FiInfo } from 'react-icons/fi';
6
- import { green, yellow, red } from '../colors';
6
+ import { clampPercent, hexToRgba, scoreToColorHex } from '../colors';
7
7
 
8
8
  type TopMover = { label?: string; uid?: string };
9
9
 
@@ -24,32 +24,15 @@ export default function RiskCard({
24
24
  topMoversTitle?: string;
25
25
  tooltipText?: string;
26
26
  }) {
27
- const pctGood = Math.max(0, Math.min(100, Math.round(Number(percentGood ?? 0))));
27
+ const pctGood = clampPercent(percentGood);
28
28
  const displayLabel = label || '';
29
29
 
30
- const hexToRgba = (hex: string, alpha: number) => {
31
- const clean = hex.replace('#', '');
32
- const r = parseInt(clean.substring(0, 2), 16);
33
- const g = parseInt(clean.substring(2, 4), 16);
34
- const b = parseInt(clean.substring(4, 6), 16);
35
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
36
- };
30
+ // Background tint uses same thresholds as GaugeCard via scoreToColorHex
37
31
 
38
- const pickTint = (score: number) => {
39
- if (score >= 75) return green;
40
- if (score >= 50) return yellow;
41
- return red;
42
- };
32
+ // Foreground bar color uses same thresholds as GaugeCard based on overall score
33
+ const activeColor = scoreToColorHex(pctGood);
43
34
 
44
- // color bands by bar position (tallest -> lowest)
45
- // indices 4 and 3 (highest bars) => red, index 2 => yellow, indices 1 and 0 => green
46
- const colorForIndex = (index: number) => {
47
- if (index >= 3) return red;
48
- if (index === 2) return yellow;
49
- return green;
50
- };
51
-
52
- const headerTint = hexToRgba(pickTint(pctGood), 0.06);
35
+ const headerTint = hexToRgba(scoreToColorHex(pctGood), 0.06);
53
36
 
54
37
  // bar heights ascending representation
55
38
  const bars = [40, 60, 85, 110, 140];
@@ -101,7 +84,7 @@ export default function RiskCard({
101
84
  style={{
102
85
  width: 36,
103
86
  height: h,
104
- backgroundColor: i === activeIndex ? colorForIndex(i) : 'var(--icon-button-secondary)',
87
+ backgroundColor: i === activeIndex ? activeColor : 'var(--icon-button-secondary)',
105
88
  borderRadius: 4,
106
89
  }}
107
90
  />
package/src/types.ts CHANGED
@@ -154,6 +154,15 @@ export interface FBIWantedMatch {
154
154
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
155
  raw_result: any;
156
156
  }
157
+ export interface EnterpriseMatch {
158
+ role: {
159
+ name: string;
160
+ description: string;
161
+ };
162
+ score: number;
163
+ description: string;
164
+ }
165
+
157
166
 
158
167
  export interface AssessmentResult {
159
168
  final_percent: number;
@@ -225,8 +234,8 @@ export interface AssessmentResult {
225
234
  skills_matrix?: SkillsMatrix;
226
235
  skills_all?: SkillsAll;
227
236
  graph_insights?: GraphInsightsPayload;
237
+ enterprise_match?: EnterpriseMatch;
228
238
  }
229
-
230
239
  export interface ClientMetadata {
231
240
  ipAddress: string;
232
241
  userAgent: string;
package/src/utils/date.ts DELETED
@@ -1,68 +0,0 @@
1
- export type DateLike = string | number | Date | null | undefined;
2
-
3
- const hasTimezoneDesignator = (value: string): boolean => {
4
- return /[zZ]$/.test(value) || /[+-]\d{2}:?\d{2}$/.test(value);
5
- };
6
-
7
- const truncateExcessFraction = (value: string): string => {
8
- // Keep at most 3 fractional digits for milliseconds
9
- return value.replace(/(\.\d{3})\d+/, '$1');
10
- };
11
-
12
- export const parseUtcDate = (input: DateLike): Date => {
13
- if (input == null) return new Date(Number.NaN);
14
- if (input instanceof Date) return new Date(input.getTime());
15
- if (typeof input === 'number') {
16
- const millis = input < 1e12 ? input * 1000 : input;
17
- return new Date(millis);
18
- }
19
-
20
- const raw = String(input).trim();
21
- if (!raw) return new Date(Number.NaN);
22
-
23
- // Numeric strings (epoch seconds or millis)
24
- if (/^\d{10}$/.test(raw)) return new Date(Number(raw) * 1000);
25
- if (/^\d{13}$/.test(raw)) return new Date(Number(raw));
26
-
27
- // Already has timezone; trust the browser parser
28
- if (hasTimezoneDesignator(raw)) return new Date(raw);
29
-
30
- // Normalize common forms: "YYYY-MM-DD HH:MM:SS[.fff...]" -> ISO-like
31
- let norm = raw.replace(' ', 'T');
32
- norm = truncateExcessFraction(norm);
33
-
34
- // Date-only string -> treat as UTC midnight
35
- if (/^\d{4}-\d{2}-\d{2}$/.test(norm)) {
36
- return new Date(`${norm}T00:00:00Z`);
37
- }
38
-
39
- // If ISO-like without timezone, explicitly mark as UTC
40
- if (/^\d{4}-\d{2}-\d{2}T/.test(norm) && !hasTimezoneDesignator(norm)) {
41
- return new Date(`${norm}Z`);
42
- }
43
-
44
- // Fallback to native parsing
45
- return new Date(norm);
46
- };
47
-
48
- export const formatLocalDate = (value: DateLike, options?: Intl.DateTimeFormatOptions): string => {
49
- const d = value instanceof Date ? value : parseUtcDate(value);
50
- if (Number.isNaN(d.getTime())) return 'N/A';
51
- return d.toLocaleDateString(undefined, options);
52
- };
53
-
54
- export const formatLocalDateTime = (value: DateLike, options?: Intl.DateTimeFormatOptions): string => {
55
- const d = value instanceof Date ? value : parseUtcDate(value);
56
- if (Number.isNaN(d.getTime())) return 'N/A';
57
- const base: Intl.DateTimeFormatOptions = {
58
- year: 'numeric',
59
- month: 'long',
60
- day: 'numeric',
61
- hour: 'numeric',
62
- minute: '2-digit',
63
- timeZoneName: 'short',
64
- };
65
- return d.toLocaleString(undefined, { ...base, ...(options || {}) });
66
- };
67
-
68
-