kyd-shared-badge 0.3.87 → 0.3.89

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.87",
3
+ "version": "0.3.89",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -21,6 +21,7 @@
21
21
  "@aws-sdk/lib-dynamodb": "^3.893.0",
22
22
  "@chatscope/chat-ui-kit-react": "^2.1.1",
23
23
  "@chatscope/chat-ui-kit-styles": "^1.4.0",
24
+ "@knowyourdeveloper/react-bubble-chart": "^1.0.1",
24
25
  "@radix-ui/react-slot": "^1.2.3",
25
26
  "@radix-ui/react-tooltip": "^1.2.8",
26
27
  "ai": "5.0.47",
@@ -73,29 +73,15 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
73
73
  </div>
74
74
  );
75
75
  })()}
76
- <div className="flex flex-col md:flex-row items-center md:items-stretch gap-6">
77
- {/* Left Half: Badge Image with robust centered overlay */}
78
- <div className="w-full md:w-1/3 flex items-center justify-center self-stretch">
79
- <div className="relative w-full max-w-xs select-none">
80
- <Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
81
- {/* Centered overlay slightly lower on Y axis, responsive and readable */}
82
- <div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
83
- <div className="font-extrabold text-black text-3xl " >
84
- {Math.round(score || 0)}%
85
- </div>
86
- </div>
87
- </div>
88
- </div>
89
-
90
- {/* Right Half: Title, Candidate, Details and Summary section */}
91
- <div className="w-full md:w-2/3">
92
- <div className={'space-y-4'}>
76
+ <div className="flex flex-col gap-6">
77
+ {/* Info section: Title, Candidate, Details and Summary */}
78
+ <div className="w-full">
79
+ <div className='space-y-2'>
93
80
  <span className='flex gap-2 w-full items-end text-start justify-start'>
94
81
  <h2 className={'text-xl font-light'} style={{ color: 'var(--text-main)' }}>KYD Candidate Report:</h2>
95
82
  <div className={'text-xl font-bold'} style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</div>
96
83
  </span>
97
- <div className={'text-sm space-y-2'}>
98
-
84
+ <div className={'text-sm'}>
99
85
  {Array.isArray(countries) && countries.length > 0 && (
100
86
  (() => {
101
87
  const countryNames = countries
@@ -145,6 +131,18 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
145
131
  )}
146
132
  </div>
147
133
  </div>
134
+
135
+ {/* Badge Image with robust centered overlay */}
136
+ <div className="w-full flex items-center justify-center self-stretch">
137
+ <div className="relative w-full max-w-xs select-none">
138
+ <Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
139
+ <div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
140
+ <div className="font-extrabold text-black text-3xl ">
141
+ {Math.round(score || 0)}%
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
148
146
  </div>
149
147
  </div>
150
148
  );
@@ -1,13 +1,15 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
3
  import React, { useMemo, useRef, useState, useEffect } from 'react';
4
- import { ResponsiveContainer, ScatterChart, Scatter, ZAxis, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
4
+ import { BubbleChart } from '@knowyourdeveloper/react-bubble-chart';
5
+ import '@knowyourdeveloper/react-bubble-chart/style.css';
5
6
 
6
7
  type SkillsRadarPoint = {
7
8
  axis: string;
8
9
  observed?: number;
9
10
  self_reported?: number;
10
11
  certified?: number;
12
+ experience?: number; // 0-100 saturation driver
11
13
  };
12
14
 
13
15
  type HoverTooltipState = {
@@ -47,8 +49,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
47
49
 
48
50
  export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
49
51
  const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
50
- const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 8);
51
-
52
+ const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
52
53
  const containerRef = useRef<HTMLDivElement>(null);
53
54
  const legendRef = useRef<HTMLDivElement>(null);
54
55
  const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
@@ -62,61 +63,98 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
62
63
  }
63
64
  }, []);
64
65
 
65
- const combinedBubbleData = useMemo(() => {
66
- return skillsRadarLimited.map((d) => {
66
+ // ratio drives size: average of observed/self_reported/certified
67
+ const bubbles = useMemo(() => {
68
+ const seriesAvg = (d: SkillsRadarPoint): number => {
67
69
  const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
68
70
  const nonZero = vals.filter((v) => v > 0);
69
71
  const base = (nonZero.length > 0 ? nonZero : vals);
70
- const avg = Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1));
71
- return { label: d.axis, value: avg };
72
+ return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
73
+ };
74
+
75
+ const maxValue = Math.max(1, ...skillsRadarLimited.map(seriesAvg));
76
+
77
+ return skillsRadarLimited.map((d) => {
78
+ const value = seriesAvg(d);
79
+ const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
80
+ const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
81
+ // map experience to saturation in HSL; keep hue constant for brand-neutral look
82
+ const saturation = Math.max(20, Math.min(100, experience));
83
+ const color = `hsl(210 ${saturation}% 45%)`;
84
+ return {
85
+ label: d.axis,
86
+ value: size,
87
+ color,
88
+ tooltip: `${d.axis}\nRatio: ${value}\nExperience: ${experience}`,
89
+ data: { ratio: value, experience }
90
+ };
72
91
  });
73
92
  }, [skillsRadarLimited]);
74
93
 
75
- const legendData = useMemo(() => {
76
- const total = combinedBubbleData.reduce((sum, d) => sum + d.value, 0);
77
- return combinedBubbleData
78
- .slice()
79
- .sort((a, b) => b.value - a.value)
80
- .map((d) => ({ label: d.label, percent: total > 0 ? Math.round((d.value / total) * 100) : 0 }));
81
- }, [combinedBubbleData]);
94
+ const bubbleData = useMemo(() => {
95
+ return bubbles.map((b) => ({
96
+ _id: String(b.label || ''),
97
+ value: Number(b.value || 0),
98
+ displayText: String(b.label || ''),
99
+ colorValue: Number(b.data?.experience || 0)
100
+ }));
101
+ }, [bubbles]);
102
+
103
+ const colorLegend = useMemo(() => {
104
+ const steps = [20, 35, 50, 65, 80, 90, 100];
105
+ return steps.map((s) => `hsl(210 ${s}% 45%)`);
106
+ }, []);
107
+
108
+ const percentLegend = useMemo(() => {
109
+ const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
110
+ return bubbles
111
+ .map((b) => ({ label: b.label, percent: total > 0 ? Math.round(((b.data?.ratio || 0) / total) * 100) : 0, experience: b.data?.experience || 0 }))
112
+ .sort((a, b) => b.percent - a.percent);
113
+ }, [bubbles]);
82
114
 
83
115
  if (!hasRadar) return null;
84
116
 
85
117
  return (
86
118
  <div className={'kyd-avoid-break'}>
87
119
  <div ref={containerRef} style={{ width: '100%', height: 340 }}>
88
- {(() => {
89
- const data = combinedBubbleData.map((d, idx) => ({ x: (idx % 4) + 1, y: Math.floor(idx / 4) + 1, value: d.value, label: d.label }));
90
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
- const BubbleTooltip = ({ active, payload }: any) => {
92
- if (!active || !payload || !payload.length) return null;
93
- const p = payload[0]?.payload;
94
- return (
95
- <div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
96
- <div style={{ fontWeight: 600 }}>{p?.label}</div>
97
- <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>Score: {p?.value}</div>
98
- </div>
99
- );
100
- };
101
- return (
102
- <ResponsiveContainer>
103
- <ScatterChart margin={{ top: 8, right: 8, bottom: 8, left: 8 }}>
104
- <CartesianGrid strokeDasharray={'3 3'} stroke={'var(--icon-button-secondary)'} />
105
- <XAxis type={'number'} dataKey={'x'} tick={false} axisLine={false} domain={[0, 5]} />
106
- <YAxis type={'number'} dataKey={'y'} tick={false} axisLine={false} domain={[0, Math.ceil(data.length / 4) + 1]} />
107
- <ZAxis dataKey={'value'} range={[80, 360]} />
108
- <Tooltip content={<BubbleTooltip />} cursor={{ stroke: 'var(--icon-button-secondary)' }} />
109
- <Scatter data={data} fill={'var(--bubble-foreground)'} />
110
- </ScatterChart>
111
- </ResponsiveContainer>
112
- );
113
- })()}
120
+ <BubbleChart
121
+ data={bubbleData}
122
+ legend={false}
123
+ tooltip
124
+ colorLegend={colorLegend}
125
+ fixedDomain={{ min: 0, max: 100 }}
126
+ onClick={(d) => {
127
+ try {
128
+ if (typeof window !== 'undefined') {
129
+ const label = String(d.displayText || d._id || '');
130
+ const anchor = `#appendix-skills-cat-${encodeURIComponent(label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
131
+ const url = `#appendix${anchor}`;
132
+ window.location.hash = url;
133
+ }
134
+ } catch {}
135
+ }}
136
+ tooltipFunc={(node, d) => {
137
+ try {
138
+ node.innerHTML = '';
139
+ const title = document.createElement('div');
140
+ title.style.fontWeight = '600';
141
+ title.textContent = String(d.displayText || d._id || '');
142
+ const body = document.createElement('div');
143
+ body.style.fontSize = '12px';
144
+ body.style.color = 'var(--text-secondary)';
145
+ const ratio = Number(d.value || 0);
146
+ const experience = Number(d.colorValue || 0);
147
+ body.textContent = `${ratio}% ratio • Experience ${experience}`;
148
+ node.appendChild(title);
149
+ node.appendChild(body);
150
+ } catch {}
151
+ }}
152
+ />
114
153
  </div>
115
- {/* Legend */}
116
154
  <div className={'mt-3'}>
117
155
  <div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
118
156
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
119
- {legendData.map((item, idx) => (
157
+ {percentLegend.map((item, idx) => (
120
158
  <button
121
159
  key={idx}
122
160
  className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
@@ -135,7 +173,7 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
135
173
  if (!rect) return;
136
174
  const x = e.clientX - rect.left + 12;
137
175
  const y = e.clientY - rect.top + 12;
138
- setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} represents ${item.percent}% of the Developer’s observable technical skills.` });
176
+ setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} ${item.percent}% of ratio Experience ${item.experience}` });
139
177
  }}
140
178
  onMouseMove={(e) => {
141
179
  if (!legendTooltip || !legendRef.current) return;
@@ -144,13 +182,24 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
144
182
  }}
145
183
  onMouseLeave={() => setLegendTooltip(null)}
146
184
  >
147
- <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--text-secondary)', flexShrink: 0 }} />
185
+ <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: `hsl(210 ${Math.max(20, Math.min(100, item.experience))}% 45%)`, flexShrink: 0 }} />
148
186
  <span className="truncate">{item.label}</span>
149
187
  <span className="ml-auto opacity-80">{item.percent}%</span>
150
188
  </button>
151
189
  ))}
152
190
  </div>
153
191
  {!headless && <TooltipBox state={legendTooltip} />}
192
+ {/* Legends */}
193
+ <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
194
+ <div className="flex items-center gap-2">
195
+ <span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 80% 45%)' }} />
196
+ <span>Bubble size: relative ratio of observed/self-reported/certified</span>
197
+ </div>
198
+ <div className="flex items-center gap-2">
199
+ <span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 20% 45%)' }} />
200
+ <span>Color saturation: experience (darker = more experienced)</span>
201
+ </div>
202
+ </div>
154
203
  </div>
155
204
  </div>
156
205
  </div>
package/src/types.ts CHANGED
@@ -378,6 +378,8 @@ export interface GraphInsightsPayload {
378
378
  observed?: number; // 0-100
379
379
  self_reported?: number; // 0-100
380
380
  certified?: number; // 0-100
381
+ // New: experience metric (0-100) for color saturation
382
+ experience?: number;
381
383
  }>;
382
384
  // New: Flattened list of business rule selections (for appendix)
383
385
  business_rules_all?: Array<{