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 +1 -1
- package/src/SharedBadgeDisplay.tsx +70 -72
- package/src/components/SkillsBubble.tsx +67 -52
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<div className={'
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<div className={'
|
|
238
|
-
<div className={'
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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="
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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;
|