kyd-shared-badge 0.3.93 → 0.3.94

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.93",
3
+ "version": "0.3.94",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -207,80 +207,78 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
207
207
  const OverviewSection = () => (
208
208
  <div className={`${wrapperMaxWidth} mx-auto mt-6`}>
209
209
  <Reveal headless={isHeadless} offsetY={8} durationMs={500}>
210
- <div className={'rounded-xl shadow-xl p-6 sm:p-8 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
211
- {/* Quadrant layout */}
212
- <div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
213
- {/* Top-left: Name, countries, badge image handled by ReportHeader */}
214
- <div>
215
- <ReportHeader
216
- badgeId={badgeId}
217
- developerName={badgeData.developerName}
218
- updatedAt={updatedAt}
219
- score={overallFinalPercent || 0}
220
- isPublic={true}
221
- badgeImageUrl={badgeData.badgeImageUrl || ''}
222
- summary={undefined}
223
- enterpriseMatch={null}
224
- countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
225
- accountAuthenticity={assessmentResult?.account_authenticity}
226
- companyName={badgeData.companyName}
227
- />
228
- </div>
229
- {/* Top-right: Role match section */}
230
- <div className={'rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
231
- <div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Role Alignment</div>
232
- {(() => {
233
- const em = assessmentResult?.enterprise_match;
234
- if (!em) return <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>No role match available.</div>;
235
- const role = em.role || {};
236
- return (
237
- <div className={'space-y-2'}>
238
- <div className={'flex items-center gap-2'}>
239
- <div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{role?.name || 'Role'}</div>
240
- <span className={'px-2 py-0.5 rounded text-xs font-semibold'} style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>{em.label}</span>
241
- </div>
242
- {em.description ? (
243
- <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{em.description}</div>
244
- ) : null}
210
+ {/* Quadrant layout */}
211
+ <div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
212
+ {/* Top-left: Name, countries, badge image handled by ReportHeader */}
213
+ <div>
214
+ <ReportHeader
215
+ badgeId={badgeId}
216
+ developerName={badgeData.developerName}
217
+ updatedAt={updatedAt}
218
+ score={overallFinalPercent || 0}
219
+ isPublic={true}
220
+ badgeImageUrl={badgeData.badgeImageUrl || ''}
221
+ summary={undefined}
222
+ enterpriseMatch={null}
223
+ countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
224
+ accountAuthenticity={assessmentResult?.account_authenticity}
225
+ companyName={badgeData.companyName}
226
+ />
227
+ </div>
228
+ {/* Top-right: Role match section */}
229
+ <div className={'rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
230
+ <div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Role Alignment</div>
231
+ {(() => {
232
+ const em = assessmentResult?.enterprise_match;
233
+ if (!em) return <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>No role match available.</div>;
234
+ const role = em.role || {};
235
+ return (
236
+ <div className={'space-y-2'}>
237
+ <div className={'flex items-center gap-2'}>
238
+ <div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{role?.name || 'Role'}</div>
239
+ <span className={'px-2 py-0.5 rounded text-xs font-semibold'} style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>{em.label}</span>
245
240
  </div>
246
- );
247
- })()}
248
- </div>
241
+ {em.description ? (
242
+ <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{em.description}</div>
243
+ ) : null}
244
+ </div>
245
+ );
246
+ })()}
247
+ </div>
249
248
 
250
- {/* Bottom-left: Technical score */}
251
- <div>
252
- {(() => {
253
- const uiTech = graphInsights?.uiSummary?.technical || {};
254
- const techPct = Math.round(Number(uiTech?.percent ?? 0));
255
- const techLabel = uiTech?.label || 'EVIDENCE';
256
- return (
257
- <GaugeCard
258
- title={'KYD Technical'}
259
- description={'Composite of technical evidence; more right indicates stronger capability'}
260
- percent={techPct}
261
- label={techLabel}
262
- topMovers={[]}
263
- />
264
- );
265
- })()}
266
- </div>
267
- {/* Bottom-right: Risk score */}
268
- <div>
269
- {(() => {
270
- const uiRisk = graphInsights?.uiSummary?.risk || {};
271
- const riskPctGood = Math.round(Number(uiRisk?.percent_good ?? 0));
272
- const riskLabel = uiRisk?.label || 'RISK';
273
- return (
274
- <RiskCard
275
- title={'KYD Risk'}
276
- description={'Lower bar height indicates lower risk exposure'}
277
- percentGood={riskPctGood}
278
- label={riskLabel}
279
- topMovers={[]}
280
- />
281
- );
282
- })()}
283
- </div>
249
+ {/* Bottom-left: Technical score */}
250
+ <div>
251
+ {(() => {
252
+ const uiTech = graphInsights?.uiSummary?.technical || {};
253
+ const techPct = Math.round(Number(uiTech?.percent ?? 0));
254
+ const techLabel = uiTech?.label || 'EVIDENCE';
255
+ return (
256
+ <GaugeCard
257
+ title={'KYD Technical'}
258
+ description={'Composite of technical evidence; more right indicates stronger capability'}
259
+ percent={techPct}
260
+ label={techLabel}
261
+ topMovers={[]}
262
+ />
263
+ );
264
+ })()}
265
+ </div>
266
+ {/* Bottom-right: Risk score */}
267
+ <div>
268
+ {(() => {
269
+ const uiRisk = graphInsights?.uiSummary?.risk || {};
270
+ const riskPctGood = Math.round(Number(uiRisk?.percent_good ?? 0));
271
+ const riskLabel = uiRisk?.label || 'RISK';
272
+ return (
273
+ <RiskCard
274
+ title={'KYD Risk'}
275
+ description={'Lower bar height indicates lower risk exposure'}
276
+ percentGood={riskPctGood}
277
+ label={riskLabel}
278
+ topMovers={[]}
279
+ />
280
+ );
281
+ })()}
284
282
  </div>
285
283
  </div>
286
284
  </Reveal>
@@ -56,12 +56,13 @@ const pickGreenByExperience = (experience: number): string => {
56
56
  return green5;
57
57
  };
58
58
 
59
- export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
59
+ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; headless?: boolean }) {
60
60
  const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
61
61
  const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
62
62
  const containerRef = useRef<HTMLDivElement>(null);
63
63
  const legendRef = useRef<HTMLDivElement>(null);
64
64
  const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
65
+ const [activeCategory, setActiveCategory] = useState<string | null>(null);
65
66
 
66
67
  useEffect(() => {
67
68
  if (typeof window !== 'undefined') {
@@ -111,18 +112,66 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
111
112
  return [green5, green4, green3, green2, green1];
112
113
  }, []);
113
114
 
114
- const percentLegend = useMemo(() => {
115
- const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
116
- return bubbles
117
- .map((b) => ({ label: b.label, percent: total > 0 ? Math.round(((b.data?.ratio || 0) / total) * 100) : 0, experience: b.data?.experience || 0 }))
118
- .sort((a, b) => b.percent - a.percent);
119
- }, [bubbles]);
115
+ const defaultActiveCategory = useMemo(() => {
116
+ let best: { label: string; ratio: number } | null = null;
117
+ for (const b of bubbles) {
118
+ const ratio = Number(b.data?.ratio || 0);
119
+ if (!best || ratio > best.ratio) best = { label: b.label, ratio };
120
+ }
121
+ return best?.label || (skillsRadarLimited[0]?.axis || null);
122
+ }, [bubbles, skillsRadarLimited]);
123
+
124
+ useEffect(() => {
125
+ if (!activeCategory && defaultActiveCategory) setActiveCategory(defaultActiveCategory);
126
+ }, [activeCategory, defaultActiveCategory]);
127
+
128
+ const skillsGrid = useMemo(() => {
129
+ const cat = activeCategory || '';
130
+ const list = (skillsByCategory && cat in (skillsByCategory || {})) ? (skillsByCategory?.[cat] || []) : [];
131
+ const items = list.slice(0, 10);
132
+ const overflow = list.length - items.length;
133
+ const display: string[] = [];
134
+ for (let i = 0; i < Math.min(9, items.length); i++) display.push(items[i]);
135
+ if (list.length > 10) {
136
+ display.push(`Others (${overflow})`);
137
+ } else if (items.length >= 10) {
138
+ display.push(items[9]);
139
+ } else {
140
+ if (items.length > 9) display.push(items[9]);
141
+ }
142
+ while (display.length < 10) display.push('');
143
+ return display;
144
+ }, [activeCategory, skillsByCategory]);
120
145
 
121
146
  if (!hasRadar) return null;
122
147
 
123
148
  return (
124
149
  <div className={'kyd-avoid-break'}>
125
- <div ref={containerRef} style={{ width: '100%', height: 340 }}>
150
+ <div ref={containerRef} style={{ width: '100%', height: 340, position: 'relative' }}>
151
+ {!headless && (
152
+ <div
153
+ className="text-xs rounded-md border shadow-sm px-2 py-1"
154
+ style={{
155
+ position: 'absolute',
156
+ right: 8,
157
+ top: 8,
158
+ zIndex: 5,
159
+ pointerEvents: 'none',
160
+ background: 'var(--content-card-background)',
161
+ borderColor: 'var(--icon-button-secondary)',
162
+ color: 'var(--text-secondary)'
163
+ }}
164
+ >
165
+ <div className="flex items-center gap-2">
166
+ <span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
167
+ <span>Size = ratio of observed/self-reported/certified</span>
168
+ </div>
169
+ <div className="flex items-center gap-2 mt-1">
170
+ <span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
171
+ <span>Color = experience (darker = more)</span>
172
+ </div>
173
+ </div>
174
+ )}
126
175
  <BubbleChart
127
176
  className='w-full h-full'
128
177
  data={bubbleData}
@@ -142,6 +191,7 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
142
191
  }}
143
192
  tooltipFunc={(node, d) => {
144
193
  try {
194
+ try { setActiveCategory(String(d.displayText || d._id || '')); } catch {}
145
195
  node.innerHTML = '';
146
196
  node.className = 'rounded-md border shadow-sm px-3 py-2 text-xs';
147
197
  (node as HTMLElement).style.background = 'var(--content-card-background)';
@@ -197,53 +247,18 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
197
247
  </div>
198
248
  <div className={'mt-3'}>
199
249
  <div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
200
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
201
- {percentLegend.map((item, idx) => (
202
- <button
203
- key={idx}
204
- className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
205
- style={{ color: 'var(--text-secondary)', background: 'transparent' }}
206
- onClick={() => {
207
- try {
208
- if (typeof window !== 'undefined') {
209
- const anchor = `#appendix-skills-cat-${encodeURIComponent(item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
210
- const url = `#appendix${anchor}`;
211
- window.location.hash = url;
212
- }
213
- } catch {}
214
- }}
215
- onMouseEnter={(e) => {
216
- const rect = legendRef.current?.getBoundingClientRect();
217
- if (!rect) return;
218
- const x = e.clientX - rect.left + 12;
219
- const y = e.clientY - rect.top + 12;
220
- setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} • ${item.percent}% of ratio • Experience ${item.experience}` });
221
- }}
222
- onMouseMove={(e) => {
223
- if (!legendTooltip || !legendRef.current) return;
224
- const rect = legendRef.current.getBoundingClientRect();
225
- setLegendTooltip({ ...legendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
226
- }}
227
- onMouseLeave={() => setLegendTooltip(null)}
228
- >
229
- <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: pickGreenByExperience(item.experience), flexShrink: 0 }} />
230
- <span className="truncate">{item.label}</span>
231
- <span className="ml-auto opacity-80">{item.percent}%</span>
232
- </button>
250
+ <div className="mb-2 text-xs font-medium" style={{ color: 'var(--text-main)' }}>
251
+ {activeCategory ? `Category: ${activeCategory}` : 'Category'}
252
+ </div>
253
+ <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs" style={{ color: 'var(--text-secondary)', height: 160 }}>
254
+ {skillsGrid.map((label, idx) => (
255
+ <div key={idx} className="flex items-center gap-2 min-w-0">
256
+ <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: label ? 'var(--icon-button-secondary)' : 'transparent', flexShrink: 0 }} />
257
+ <span className="truncate" title={label}>{label || '\u00A0'}</span>
258
+ </div>
233
259
  ))}
234
260
  </div>
235
261
  {!headless && <TooltipBox state={legendTooltip} />}
236
- {/* Legends */}
237
- <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
238
- <div className="flex items-center gap-2">
239
- <span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
240
- <span>Bubble size: relative ratio of observed/self-reported/certified</span>
241
- </div>
242
- <div className="flex items-center gap-2">
243
- <span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
244
- <span>Color shade: experience (darker = more experienced)</span>
245
- </div>
246
- </div>
247
262
  </div>
248
263
  </div>
249
264
  </div>
package/src/types.ts CHANGED
@@ -381,6 +381,8 @@ export interface GraphInsightsPayload {
381
381
  // New: experience metric (0-100) for color saturation
382
382
  experience?: number;
383
383
  }>;
384
+ // New: mapping of category -> list of skills contributing to that category
385
+ skillsByCategory?: Record<string, string[]>;
384
386
  // New: Flattened list of business rule selections (for appendix)
385
387
  business_rules_all?: Array<{
386
388
  provider: string;