kyd-shared-badge 0.3.87 → 0.3.88

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.88",
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.0",
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,14 @@
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
5
 
6
6
  type SkillsRadarPoint = {
7
7
  axis: string;
8
8
  observed?: number;
9
9
  self_reported?: number;
10
10
  certified?: number;
11
+ experience?: number; // 0-100 saturation driver
11
12
  };
12
13
 
13
14
  type HoverTooltipState = {
@@ -47,8 +48,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
47
48
 
48
49
  export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
49
50
  const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
50
- const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 8);
51
-
51
+ const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
52
52
  const containerRef = useRef<HTMLDivElement>(null);
53
53
  const legendRef = useRef<HTMLDivElement>(null);
54
54
  const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
@@ -62,61 +62,71 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
62
62
  }
63
63
  }, []);
64
64
 
65
- const combinedBubbleData = useMemo(() => {
66
- return skillsRadarLimited.map((d) => {
65
+ // ratio drives size: average of observed/self_reported/certified
66
+ const bubbles = useMemo(() => {
67
+ const seriesAvg = (d: SkillsRadarPoint): number => {
67
68
  const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
68
69
  const nonZero = vals.filter((v) => v > 0);
69
70
  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 };
71
+ return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
72
+ };
73
+
74
+ const maxValue = Math.max(1, ...skillsRadarLimited.map(seriesAvg));
75
+
76
+ return skillsRadarLimited.map((d) => {
77
+ const value = seriesAvg(d);
78
+ const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
79
+ const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
80
+ // map experience to saturation in HSL; keep hue constant for brand-neutral look
81
+ const saturation = Math.max(20, Math.min(100, experience));
82
+ const color = `hsl(210 ${saturation}% 45%)`;
83
+ return {
84
+ label: d.axis,
85
+ value: size,
86
+ color,
87
+ tooltip: `${d.axis}\nRatio: ${value}\nExperience: ${experience}`,
88
+ data: { ratio: value, experience }
89
+ };
72
90
  });
73
91
  }, [skillsRadarLimited]);
74
92
 
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]);
93
+ const percentLegend = useMemo(() => {
94
+ const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
95
+ return bubbles
96
+ .map((b) => ({ label: b.label, percent: total > 0 ? Math.round(((b.data?.ratio || 0) / total) * 100) : 0, experience: b.data?.experience || 0 }))
97
+ .sort((a, b) => b.percent - a.percent);
98
+ }, [bubbles]);
82
99
 
83
100
  if (!hasRadar) return null;
84
101
 
85
102
  return (
86
103
  <div className={'kyd-avoid-break'}>
87
104
  <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
- })()}
105
+ <BubbleChart
106
+ width={'100%'}
107
+ height={'100%'}
108
+ graph={{ zoom: 1 }}
109
+ showLegend={false}
110
+ data={bubbles}
111
+ legend={false}
112
+ valueFont={{ family: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, sans-serif', size: 11, color: 'var(--text-main)' }}
113
+ labelFont={{ family: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, sans-serif', size: 12, color: 'var(--text-main)' }}
114
+ bubbleClickFun={(_evt: unknown, b: { label?: string }) => {
115
+ try {
116
+ if (typeof window !== 'undefined') {
117
+ const anchor = `#appendix-skills-cat-${encodeURIComponent(String(b?.label || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
118
+ const url = `#appendix${anchor}`;
119
+ window.location.hash = url;
120
+ }
121
+ } catch {}
122
+ }}
123
+ tooltip
124
+ />
114
125
  </div>
115
- {/* Legend */}
116
126
  <div className={'mt-3'}>
117
127
  <div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
118
128
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
119
- {legendData.map((item, idx) => (
129
+ {percentLegend.map((item, idx) => (
120
130
  <button
121
131
  key={idx}
122
132
  className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
@@ -135,7 +145,7 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
135
145
  if (!rect) return;
136
146
  const x = e.clientX - rect.left + 12;
137
147
  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.` });
148
+ setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} ${item.percent}% of ratio Experience ${item.experience}` });
139
149
  }}
140
150
  onMouseMove={(e) => {
141
151
  if (!legendTooltip || !legendRef.current) return;
@@ -144,13 +154,24 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
144
154
  }}
145
155
  onMouseLeave={() => setLegendTooltip(null)}
146
156
  >
147
- <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--text-secondary)', flexShrink: 0 }} />
157
+ <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
158
  <span className="truncate">{item.label}</span>
149
159
  <span className="ml-auto opacity-80">{item.percent}%</span>
150
160
  </button>
151
161
  ))}
152
162
  </div>
153
163
  {!headless && <TooltipBox state={legendTooltip} />}
164
+ {/* Legends */}
165
+ <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
166
+ <div className="flex items-center gap-2">
167
+ <span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 80% 45%)' }} />
168
+ <span>Bubble size: relative ratio of observed/self-reported/certified</span>
169
+ </div>
170
+ <div className="flex items-center gap-2">
171
+ <span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 20% 45%)' }} />
172
+ <span>Color saturation: experience (darker = more experienced)</span>
173
+ </div>
174
+ </div>
154
175
  </div>
155
176
  </div>
156
177
  </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<{