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
|
@@ -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
|
|
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
|
|
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-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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={
|
|
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={
|
|
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
|
|
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
|
|
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
|
|
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">
|