kyd-shared-badge 0.3.100 → 0.3.101

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.100",
3
+ "version": "0.3.101",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -81,7 +81,8 @@ export default function RoleOverviewCard({
81
81
  <div className="relative" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth: 360 }}>
82
82
  <GaugeComponent
83
83
  type="semicircle"
84
- style={{ width: '100%', height: '100%' }}
84
+ style={{ width: 'calc(100% - 16px)', height: '100%', marginLeft: 8, marginRight: 8 }}
85
+ marginInPercent={{ top: 0.08, bottom: 0.0, left: 0.1, right: 0.1 }}
85
86
  value={pct}
86
87
  minValue={0}
87
88
  maxValue={100}
@@ -13,6 +13,8 @@ type SkillsRadarPoint = {
13
13
  self_reported?: number;
14
14
  certified?: number;
15
15
  experience?: number; // 0-100 saturation driver
16
+ // Total evidence count (backend computed) per category
17
+ evidence_count_total?: number;
16
18
  };
17
19
 
18
20
  type HoverTooltipState = {
@@ -50,7 +52,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
50
52
  };
51
53
 
52
54
 
53
- export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[] }>; headless?: boolean }) {
55
+ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[]; evidenceCount?: number }>; headless?: boolean }) {
54
56
  const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
55
57
  const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
56
58
  const containerRef = useRef<HTMLDivElement>(null);
@@ -85,7 +87,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
85
87
  };
86
88
  const hideLegendTooltip = () => setLegendTooltip(null);
87
89
 
88
- // ratio drives size: average of observed/self_reported/certified
90
+ // ratio drives size by default; prefer backend evidence_count_total when present
89
91
  const bubbles = useMemo(() => {
90
92
  const seriesAvg = (d: SkillsRadarPoint): number => {
91
93
  const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
@@ -94,10 +96,19 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
94
96
  return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
95
97
  };
96
98
 
97
- const maxValue = Math.max(1, ...skillsRadarLimited.map(seriesAvg));
99
+ // Prefer evidence_count_total across categories if available; fallback to ratio
100
+ const evidenceTotals = (skillsCategoryRadar || []).map((d) => Number((d as any).evidence_count_total || 0));
101
+ const evidenceAvailable = evidenceTotals.some((v) => v > 0);
102
+
103
+ const maxValue = evidenceAvailable
104
+ ? Math.max(1, ...evidenceTotals)
105
+ : Math.max(1, ...skillsRadarLimited.map(seriesAvg));
98
106
 
99
107
  return skillsRadarLimited.map((d) => {
100
- const value = seriesAvg(d);
108
+ const ratio = seriesAvg(d);
109
+ const evidenceCountTotal = Number((d as any).evidence_count_total || 0);
110
+ const valueRaw = evidenceAvailable ? evidenceCountTotal : ratio;
111
+ const value = Math.max(0, Number(valueRaw));
101
112
  const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
102
113
  const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
103
114
  const color = 'var(--content-card-background)';
@@ -105,11 +116,13 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
105
116
  label: d.axis,
106
117
  value: size,
107
118
  color,
108
- tooltip: `${d.axis}\nRatio: ${value}\nExperience: ${experience}`,
109
- data: { ratio: value, experience }
119
+ tooltip: evidenceAvailable
120
+ ? `${d.axis}\nEvidence: ${value} sources\nExperience: ${experience}`
121
+ : `${d.axis}\nRatio: ${ratio}\nExperience: ${experience}`,
122
+ data: { ratio, experience, evidence: value }
110
123
  };
111
124
  });
112
- }, [skillsRadarLimited]);
125
+ }, [skillsRadarLimited, skillsCategoryRadar]);
113
126
 
114
127
  const bubbleData = useMemo(() => {
115
128
  return bubbles.map((b) => ({
@@ -245,39 +258,70 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
245
258
  <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: entry ? 'var(--icon-button-secondary)' : 'transparent', flexShrink: 0 }} />
246
259
  <span className="shrink-0 opacity-70 ">{idx + (isLeft ? 1 : 6)}.</span>
247
260
  {entry && typeof entry !== 'string' ? (
248
- <span className="truncate" title={entry.label}>{entry.label}</span>
261
+ <span className="truncate" title={entry.label}>
262
+ {entry.label}
263
+ {/* Evidence count bullet */}
264
+ {(() => {
265
+ const meta = skillsMeta?.[entry.label];
266
+ const count = Number((meta as any)?.evidenceCount || 0);
267
+ return count > 0 ? (
268
+ <>
269
+ {' '}<span className="opacity-60">•</span>{' '}
270
+ <span
271
+ className="underline decoration-dotted underline-offset-2 cursor-help opacity-80"
272
+ onMouseEnter={(e) =>
273
+ showLegendTooltipAt(
274
+ e.currentTarget,
275
+ 'Evidence count',
276
+ 'Total number of sources across observed, certified, and self-reported evidence for this skill.'
277
+ )
278
+ }
279
+ onMouseLeave={hideLegendTooltip}
280
+ >
281
+ {count}
282
+ </span>
283
+ </>
284
+ ) : null;
285
+ })()}
286
+ </span>
249
287
  ) : (
250
288
  <span className="truncate">{typeof entry === 'string' ? entry : '\u00A0'}</span>
251
289
  )}
252
290
  </div>
253
291
  <span className="text-xs text-[var(--text-secondary)] flex flex-wrap items-center gap-1">
254
- <span
255
- className="underline decoration-dotted underline-offset-2 cursor-help"
256
- onMouseEnter={(e) => showLegendTooltipAt(e.currentTarget, 'Sources', 'The source where we observed this skill.')}
257
- onMouseLeave={hideLegendTooltip}
258
- >
259
- Sources
260
- </span>:
261
- {Array.isArray((entry as any).sources) && (entry as any).sources.length > 0 ? (
262
- (() => {
263
- const sourceProviders: string[] = ((entry as any).sources as string[]).map((src: string) => {
264
- const str = String(src);
265
- let provider = str.split(':')[0] || '';
266
- if (!provider || provider === str) {
267
- // If split(':')[0] didn't find a delimiter or provider (i.e., no ':'), try split('.')
268
- provider = str.split('.')[0] || '';
269
- }
270
- return provider.toLowerCase();
271
- });
272
- const uniqueProviders = Array.from(new Set<string>(sourceProviders));
273
- const filteredProviders = uniqueProviders.filter((provider) =>
274
- providers.includes(provider.toLowerCase())
275
- );
276
- return filteredProviders.map((provider) => (
277
- <ProviderIcon key={provider} name={provider} />
278
- ));
279
- })()
280
- ) : null}
292
+ {entry && typeof entry !== 'string' ? (
293
+ <>
294
+ <span
295
+ className="underline decoration-dotted underline-offset-2 cursor-help"
296
+ onMouseEnter={(e) => showLegendTooltipAt(e.currentTarget, 'Sources', 'The source where we observed this skill.')}
297
+ onMouseLeave={hideLegendTooltip}
298
+ >
299
+ Sources
300
+ </span>:
301
+ {Array.isArray((entry as any).sources) && (entry as any).sources.length > 0 ? (
302
+ (() => {
303
+ const sourceProviders: string[] = ((entry as any).sources as string[]).map((src: string) => {
304
+ const str = String(src);
305
+ let provider = str.split(':')[0] || '';
306
+ if (!provider || provider === str) {
307
+ // If split(':')[0] didn't find a delimiter or provider (i.e., no ':'), try split('.')
308
+ provider = str.split('.')[0] || '';
309
+ }
310
+ return provider.toLowerCase();
311
+ });
312
+ const uniqueProviders = Array.from(new Set<string>(sourceProviders));
313
+ const filteredProviders = uniqueProviders.filter((provider) =>
314
+ providers.includes(provider.toLowerCase())
315
+ );
316
+ return filteredProviders.map((provider) => (
317
+ <ProviderIcon key={provider} name={provider} />
318
+ ));
319
+ })()
320
+ ) : null}
321
+ </>
322
+ ) : (
323
+ <span className="opacity-0 whitespace-nowrap">'\u00A0'</span>
324
+ )}
281
325
  </span>
282
326
  </div>
283
327
  {entry && typeof entry !== 'string' ? (
@@ -348,7 +392,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
348
392
  >
349
393
  <div className="flex items-center gap-2">
350
394
  <span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
351
- <span>Size = ratio of observed/self-reported/certified</span>
395
+ <span>Size = evidence count per category</span>
352
396
  </div>
353
397
  <div className="flex items-center gap-2 mt-1">
354
398
  <span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
package/src/types.ts CHANGED
@@ -397,11 +397,21 @@ export interface GraphInsightsPayload {
397
397
  certified?: number; // 0-100
398
398
  // New: experience metric (0-100) for color saturation
399
399
  experience?: number;
400
+ // New: total evidence count across all skills in this category
401
+ evidence_count_total?: number;
400
402
  }>;
401
403
  // New: mapping of category -> list of skills contributing to that category
402
404
  skillsByCategory?: Record<string, string[]>;
403
405
  // New: per-skill metadata used by UI (e.g., presence label, experience years)
404
- skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; years?: number; sources?: string[] }>;
406
+ skillsMeta?: Record<string, {
407
+ presence?: 'certified' | 'observed' | 'self-reported';
408
+ // New: list of presence types for multi-dot rendering
409
+ presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>;
410
+ years?: number;
411
+ sources?: string[];
412
+ // New: total number of evidence sources across observed/self-reported/certified
413
+ evidenceCount?: number;
414
+ }>;
405
415
  // New: Flattened list of business rule selections (for appendix)
406
416
  business_rules_all?: Array<{
407
417
  provider: string;