kyd-shared-badge 0.3.9 → 0.3.10

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.9",
3
+ "version": "0.3.10",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -213,7 +213,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
213
213
  return (
214
214
  <GaugeCard
215
215
  key={'ai-card'}
216
- title={'KYD AI'}
216
+ title={'KYD AI (Beta)'}
217
217
  description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
218
218
  percent={ai_usage_summary?.transparency_score}
219
219
  label={label}
@@ -249,7 +249,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
249
249
  {/* Left: Bars */}
250
250
  <Reveal className="lg:col-span-8 h-full">
251
251
  <CategoryBars
252
- title={'Technical Category Contributions - Percentages'}
252
+ title={'Technical Category Contributions'}
253
253
  categories={genreMapping?.['Technical'] as string[]}
254
254
  categoryScores={categoryScores}
255
255
  barColor={barColor}
@@ -466,13 +466,16 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
466
466
 
467
467
 
468
468
  <div className="pt-8">
469
- <h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix: Data Sources</h3>
469
+ <h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix</h3>
470
470
  <div className="space-y-8">
471
471
 
472
472
  {/* Skills */}
473
473
  <Reveal>
474
474
  <div>
475
- <h4 id="appendix-skills" className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Skills</h4>
475
+ <h4 id="appendix-skills" className={'text-lg font-bold mb-1'} style={{ color: 'var(--text-main)' }}>Skills</h4>
476
+ <div className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
477
+ Skills are grouped by evidence: Observed when demonstrated in code, Self-reported when declared by the developer without independent verification, and Certified when confirmed through a credential. These categories distinguish between use, claim, and third-party validation.
478
+ </div>
476
479
  <SkillsAppendixTable skillsAll={skillsAll} />
477
480
  </div>
478
481
  </Reveal>
package/src/colors.ts CHANGED
@@ -41,3 +41,27 @@ export { red, yellow, green }
41
41
  // #D5E8E2
42
42
 
43
43
  // #EEF6F4
44
+
45
+ const green1 = '#02a389'
46
+ const green2 = '#7BB9AA'
47
+ const green3 = '#A9D1C6'
48
+ const green4 = '#D5E8E2'
49
+ const green5 = '#EEF6F4'
50
+
51
+ export { green1, green2, green3, green4, green5 }
52
+
53
+ const yellow1 = '#F4BE66'
54
+ const yellow2 = '#F7CF8F'
55
+ const yellow3 = '#F9DFB6'
56
+ const yellow4 = '#FDEFDB'
57
+ const yellow5 = '#FEF9F0'
58
+
59
+ export { yellow1, yellow2, yellow3, yellow4, yellow5 }
60
+
61
+ const red1 = '#EC6662'
62
+ const red2 = '#F1908C'
63
+ const red3 = '#F5B7B3'
64
+ const red4 = '#FADBDA'
65
+ const red5 = '#FDF0EF'
66
+
67
+ export { red1, red2, red3, red4, red5 }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
+ import { green, red } from '../colors';
4
5
 
5
6
  type CategoryBarsProps = {
6
7
  title: string;
@@ -22,6 +23,9 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
22
23
  return (
23
24
  <div className="relative flex flex-col h-full">
24
25
  <div className="font-semibold text-xl mb-2" style={{ color: 'var(--text-main)' }}>{title}</div>
26
+ <div className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
27
+ Each bar represents a category's net evidence: positive values extend right in green, negative values extend left, and bar length denotes contribution magnitude.
28
+ </div>
25
29
  <div className="flex-1 flex flex-col justify-between relative">
26
30
  <div
27
31
  className="absolute top-0 bottom-0 w-px"
@@ -53,11 +57,11 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
53
57
  const fillWidth = absPercent / 2; // half-bar represents 100%
54
58
  const left = isNegative ? `calc(50% - ${fillWidth}%)` : '50%';
55
59
  return (
56
- <div key={category} className="first:pt-0">
60
+ <div key={category} className="first:pt-0 group relative">
57
61
  <div className={'font-semibold mb-1'} style={{ color: 'var(--text-main)' }}>
58
62
  {category}
59
63
  </div>
60
- <div className="relative group">
64
+ <div className="relative">
61
65
  <div
62
66
  className="w-full rounded-full overflow-hidden relative"
63
67
  style={{
@@ -67,23 +71,23 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
67
71
  }}
68
72
  >
69
73
  {/* signed fill originating from center */}
70
- <div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ? 'var(--status-negative, #EC6662)' : 'var(--status-positive, #02a389)' }} />
74
+ <div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ? `var(--status-negative, ${red})` : `var(--status-positive, ${green})` }} />
71
75
  </div>
72
- <div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
73
- <div
74
- style={{
75
- background: 'var(--content-card-background)',
76
- border: '1px solid var(--icon-button-secondary)',
77
- color: 'var(--text-main)',
78
- padding: 10,
79
- borderRadius: 6,
80
- }}
81
- >
82
- <div style={{ fontWeight: 600 }}>{category}</div>
83
- <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{label}</div>
84
- <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
85
- {getCategoryTooltipCopy(category)}
86
- </div>
76
+ </div>
77
+ <div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
78
+ <div
79
+ style={{
80
+ background: 'var(--content-card-background)',
81
+ border: '1px solid var(--icon-button-secondary)',
82
+ color: 'var(--text-main)',
83
+ padding: 10,
84
+ borderRadius: 6,
85
+ }}
86
+ >
87
+ <div style={{ fontWeight: 600 }}>{category}</div>
88
+ <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{label}</div>
89
+ <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
90
+ {getCategoryTooltipCopy(category)}
87
91
  </div>
88
92
  </div>
89
93
  </div>
@@ -3,9 +3,24 @@
3
3
  import React from 'react';
4
4
  import BusinessRuleLink from './BusinessRuleLink';
5
5
  import { FiInfo } from 'react-icons/fi';
6
+ import { green, yellow, red } from '../colors';
6
7
 
7
8
  type TopMover = { label?: string; uid?: string };
8
9
 
10
+ const hexToRgba = (hex: string, alpha: number) => {
11
+ const clean = hex.replace('#', '');
12
+ const r = parseInt(clean.substring(0, 2), 16);
13
+ const g = parseInt(clean.substring(2, 4), 16);
14
+ const b = parseInt(clean.substring(4, 6), 16);
15
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
16
+ };
17
+
18
+ const pickTint = (score: number) => {
19
+ if (score >= 75) return green;
20
+ if (score >= 50) return yellow;
21
+ return red;
22
+ };
23
+
9
24
  export default function GaugeCard({
10
25
  title,
11
26
  description,
@@ -31,6 +46,13 @@ export default function GaugeCard({
31
46
  const circumference = Math.PI * radius;
32
47
  const progress = pct / 100;
33
48
  const dash = circumference * progress;
49
+ const progressColor =
50
+ pct <= 33
51
+ ? `var(--status-negative, ${red})`
52
+ : pct <= 66
53
+ ? `var(--status-neutral, ${yellow})`
54
+ : `var(--status-positive, ${green})`;
55
+ const headerTint = hexToRgba(pickTint(pct), 0.06);
34
56
 
35
57
  return (
36
58
  <div
@@ -38,6 +60,7 @@ export default function GaugeCard({
38
60
  style={{
39
61
  backgroundColor: 'var(--content-card-background)',
40
62
  borderColor: 'var(--icon-button-secondary)',
63
+ backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
41
64
  }}
42
65
  >
43
66
  <div className="mb-3 flex items-start justify-between gap-2">
@@ -64,7 +87,7 @@ export default function GaugeCard({
64
87
  <div className="relative group" style={{ width: size, height: size / 2 }}>
65
88
  <svg width={size} height={size / 2} viewBox={`0 0 ${size} ${size/2}`}>
66
89
  <path d={`M ${strokeWidth/2} ${size/2} A ${radius} ${radius} 0 0 1 ${size-strokeWidth/2} ${size/2}`} stroke={'var(--icon-button-secondary)'} strokeWidth={strokeWidth} fill="none" strokeLinecap="round" />
67
- <path d={`M ${strokeWidth/2} ${size/2} A ${radius} ${radius} 0 0 1 ${size-strokeWidth/2} ${size/2}`} stroke={'var(--bubble-foreground)'} strokeWidth={strokeWidth} fill="none" strokeLinecap="round" strokeDasharray={`${dash},${circumference}`} />
90
+ <path d={`M ${strokeWidth/2} ${size/2} A ${radius} ${radius} 0 0 1 ${size-strokeWidth/2} ${size/2}`} stroke={progressColor} strokeWidth={strokeWidth} fill="none" strokeLinecap="round" strokeDasharray={`${dash},${circumference}`} />
68
91
  <line x1={size/2} y1={size/2} x2={size/2 + radius * Math.cos(Math.PI * progress - Math.PI)} y2={size/2 + radius * Math.sin(Math.PI * progress - Math.PI)} stroke={'var(--text-main)'} strokeWidth="2" />
69
92
  </svg>
70
93
  {(tooltipText || description) && (
@@ -3,6 +3,7 @@
3
3
  import React from 'react';
4
4
  // import { red, yellow, green } from '@/components/badge/colors';
5
5
  import { GraphInsightsPayload, ScoringSummary } from '../types';
6
+ import { green1, green2, green3, green4, green5 } from '../colors';
6
7
  import {
7
8
  ResponsiveContainer,
8
9
  Tooltip,
@@ -188,6 +189,46 @@ const GraphInsights = ({
188
189
 
189
190
  if (!graphInsights) return null;
190
191
 
192
+ // Tooltip for axis labels (outside of Recharts' built-in hover zones)
193
+ const [labelHover, setLabelHover] = React.useState<
194
+ | {
195
+ x: number;
196
+ y: number;
197
+ name: string;
198
+ score: number;
199
+ avg?: number;
200
+ }
201
+ | null
202
+ >(null);
203
+
204
+ const radarLookup = React.useMemo(() => {
205
+ const map = new Map<string, { score: number; avg?: number }>();
206
+ for (const d of radarData) map.set(d.axis, { score: d.score, avg: d.avg });
207
+ return map;
208
+ }, [radarData]);
209
+
210
+ // Custom tick renderer to attach hover handlers to category names
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ const renderAngleTick = (props: any) => {
213
+ const { payload, x, y, textAnchor } = props;
214
+ const name: string = payload?.value ?? '';
215
+ const info = radarLookup.get(name) || { score: 0, avg: undefined };
216
+ return (
217
+ <text
218
+ x={x}
219
+ y={y}
220
+ textAnchor={textAnchor}
221
+ dominantBaseline="central"
222
+ style={{ fill: 'var(--text-secondary)', fontSize: 12, cursor: 'default' }}
223
+ onMouseEnter={() => setLabelHover({ x, y, name, score: info.score, avg: info.avg })}
224
+ onMouseMove={() => setLabelHover({ x, y, name, score: info.score, avg: info.avg })}
225
+ onMouseLeave={() => setLabelHover(null)}
226
+ >
227
+ {name}
228
+ </text>
229
+ );
230
+ };
231
+
191
232
  return (
192
233
  <div className="grid grid-cols-1 gap-6">
193
234
  {/* Spider Chart: Category Balance (genre-scoped) */}
@@ -197,7 +238,7 @@ const GraphInsights = ({
197
238
 
198
239
  {/* Spider Chart: Category Scores */}
199
240
  <div className="" style={{ width: '100%', height: 450 }}>
200
- <div className="" style={{ width: '100%', height: 375 }}>
241
+ <div className="relative" style={{ width: '100%', height: 375 }}>
201
242
  <div className="mb-2">
202
243
  <div className={'font-medium'} style={{ color: 'var(--text-main)' }}>{genre ? `${genre} ` : ''} Category Contributions - Percentages</div>
203
244
  <div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>The spider diagram displays the KYD {genre} score across its sub-categories, with each point representing the strength of available evidence signals</div>
@@ -205,7 +246,7 @@ const GraphInsights = ({
205
246
  <ResponsiveContainer>
206
247
  <RadarChart data={radarData} cx="50%" cy="50%" outerRadius="70%">
207
248
  <PolarGrid stroke="var(--icon-button-secondary)" />
208
- <PolarAngleAxis dataKey="axis" tick={{ fill: 'var(--text-secondary)', fontSize: 12 }} />
249
+ <PolarAngleAxis dataKey="axis" tick={renderAngleTick} />
209
250
  <PolarRadiusAxis angle={55} domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} className="text-sm" />
210
251
  {/* Primary area */}
211
252
  <Radar name="Category Score" dataKey="score" stroke={'var(--text-main)'} fill={'var(--text-main)'} fillOpacity={0.22} />
@@ -214,6 +255,23 @@ const GraphInsights = ({
214
255
  <Tooltip content={<RadarCategoryTooltip />} />
215
256
  </RadarChart>
216
257
  </ResponsiveContainer>
258
+ {labelHover && (
259
+ <div
260
+ className="pointer-events-none absolute z-30"
261
+ style={{
262
+ left: Math.max(0, labelHover.x - 150),
263
+ top: Math.max(0, labelHover.y + 8),
264
+ }}
265
+ >
266
+ <div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6, maxWidth: 320 }}>
267
+ <div style={{ fontWeight: 600 }}>{labelHover.name}</div>
268
+ <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
269
+ Score: {Math.round(labelHover.score)}{labelHover.avg !== undefined ? ` • Avg: ${Math.round(labelHover.avg)}` : ''}
270
+ </div>
271
+ <div style={{ marginTop: 8, fontSize: 12, color: 'var(--text-secondary)' }}>{getCategoryTooltipCopy(labelHover.name)}</div>
272
+ </div>
273
+ </div>
274
+ )}
217
275
  </div>
218
276
  {/* Custom legend below the spider chart */}
219
277
  <div className="mt-3 flex items-center justify-center gap-4" style={{ color: 'var(--text-secondary)', fontSize: 12 }}>
@@ -238,10 +296,7 @@ const GraphInsights = ({
238
296
  <ResponsiveContainer>
239
297
  <PieChart>
240
298
  {(() => {
241
- const isDarkMode = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
242
- const palette = !isDarkMode
243
- ? ['#E0E0E0', '#BDBDBD', '#9E9E9E', '#757575', '#616161', '#424242', '#212121']
244
- : ['#212121', '#424242', '#616161', '#757575', '#9E9E9E', '#BDBDBD', '#E0E0E0'];
299
+ const palette = [green1, green2, green3, green4, green5];
245
300
  const total = categoryData.reduce((sum, d) => sum + (d.value || 0), 0);
246
301
  const pieData = categoryData.map(d => ({
247
302
  name: d.category,
@@ -249,7 +304,7 @@ const GraphInsights = ({
249
304
  share: total > 0 ? (d.value / total) : 0,
250
305
  }));
251
306
 
252
- // Smaller, theme-aware labels
307
+ // Smaller labels
253
308
  const renderLabel = (props: {x: number, y: number, textAnchor: string, percent: number, name: string, payload: {name: string}}) => {
254
309
  const pct = Math.round(((props?.percent ?? 0) * 100));
255
310
  if (pct < 4) return null;
@@ -290,10 +345,7 @@ const GraphInsights = ({
290
345
  {/* Legend below the pie chart: includes 0% categories */}
291
346
  <div className="mt-3">
292
347
  {(() => {
293
- const isDarkMode = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
294
- const palette = !isDarkMode
295
- ? ['#E0E0E0', '#BDBDBD', '#9E9E9E', '#757575', '#616161', '#424242', '#212121']
296
- : ['#212121', '#424242', '#616161', '#757575', '#9E9E9E', '#BDBDBD', '#E0E0E0'];
348
+ const palette = [green1, green2, green3, green4, green5];
297
349
  const pieNames = categoryData.map((d) => d.category);
298
350
  const getColor = (name: string) => {
299
351
  const idx = pieNames.indexOf(name);
@@ -3,6 +3,7 @@
3
3
  import React from 'react';
4
4
  import BusinessRuleLink from './BusinessRuleLink';
5
5
  import { FiInfo } from 'react-icons/fi';
6
+ import { green, yellow, red } from '../colors';
6
7
 
7
8
  type TopMover = { label?: string; uid?: string };
8
9
 
@@ -26,6 +27,22 @@ export default function RiskCard({
26
27
  const pctGood = Math.max(0, Math.min(100, Math.round(Number(percentGood ?? 0))));
27
28
  const displayLabel = label || '';
28
29
 
30
+ const hexToRgba = (hex: string, alpha: number) => {
31
+ const clean = hex.replace('#', '');
32
+ const r = parseInt(clean.substring(0, 2), 16);
33
+ const g = parseInt(clean.substring(2, 4), 16);
34
+ const b = parseInt(clean.substring(4, 6), 16);
35
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
36
+ };
37
+
38
+ const pickTint = (score: number) => {
39
+ if (score >= 75) return green;
40
+ if (score >= 50) return yellow;
41
+ return red;
42
+ };
43
+
44
+ const headerTint = hexToRgba(pickTint(pctGood), 0.06);
45
+
29
46
  // bar heights ascending representation
30
47
  const bars = [40, 60, 85, 110, 140];
31
48
  let activeIndex = 0; // Default to the shortest bar (highest risk)
@@ -45,6 +62,7 @@ export default function RiskCard({
45
62
  style={{
46
63
  backgroundColor: 'var(--content-card-background)',
47
64
  borderColor: 'var(--icon-button-secondary)',
65
+ backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
48
66
  }}
49
67
  >
50
68
  <div className="mb-3 flex items-start justify-between gap-2">