kyd-shared-badge 0.3.8 → 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.8",
3
+ "version": "0.3.10",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -23,6 +23,13 @@ import { BusinessRulesProvider } from './components/BusinessRulesContext';
23
23
  import Reveal from './components/Reveal';
24
24
  import { formatLocalDateTime } from './utils/date';
25
25
  import ChatWidget from './chat/ChatWidget';
26
+ type ChatWidgetProps = Partial<{
27
+ api: string;
28
+ title: string;
29
+ hintText: string;
30
+ loginPath: string;
31
+ headerOffset: 'auto' | 'none' | number;
32
+ }>;
26
33
 
27
34
  // const hexToRgba = (hex: string, alpha: number) => {
28
35
  // const clean = hex.replace('#', '');
@@ -32,7 +39,7 @@ import ChatWidget from './chat/ChatWidget';
32
39
  // return `rgba(${r}, ${g}, ${b}, ${alpha})`;
33
40
  // };
34
41
 
35
- const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
42
+ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeData, chatProps?: ChatWidgetProps }) => {
36
43
  const {
37
44
  badgeId,
38
45
  developerName,
@@ -206,7 +213,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
206
213
  return (
207
214
  <GaugeCard
208
215
  key={'ai-card'}
209
- title={'KYD AI'}
216
+ title={'KYD AI (Beta)'}
210
217
  description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
211
218
  percent={ai_usage_summary?.transparency_score}
212
219
  label={label}
@@ -242,7 +249,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
242
249
  {/* Left: Bars */}
243
250
  <Reveal className="lg:col-span-8 h-full">
244
251
  <CategoryBars
245
- title={'Technical Category Contributions - Percentages'}
252
+ title={'Technical Category Contributions'}
246
253
  categories={genreMapping?.['Technical'] as string[]}
247
254
  categoryScores={categoryScores}
248
255
  barColor={barColor}
@@ -459,13 +466,16 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
459
466
 
460
467
 
461
468
  <div className="pt-8">
462
- <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>
463
470
  <div className="space-y-8">
464
471
 
465
472
  {/* Skills */}
466
473
  <Reveal>
467
474
  <div>
468
- <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>
469
479
  <SkillsAppendixTable skillsAll={skillsAll} />
470
480
  </div>
471
481
  </Reveal>
@@ -534,7 +544,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
534
544
  </footer>
535
545
  </Reveal>
536
546
  {/* Floating chat widget */}
537
- <ChatWidget api={'/api/chat'} badgeId={badgeId} />
547
+ <ChatWidget api={chatProps?.api || '/api/chat'} badgeId={badgeId} title={chatProps?.title} hintText={chatProps?.hintText} loginPath={chatProps?.loginPath} headerOffset={chatProps?.headerOffset} />
538
548
  </div>
539
549
  </BusinessRulesProvider>
540
550
  );
@@ -20,9 +20,12 @@ type Props = {
20
20
  title?: string;
21
21
  hintText?: string;
22
22
  badgeId: string;
23
+ // Optional: customize login path and header offset behavior for different host apps
24
+ loginPath?: string; // e.g., '/login' in enterprise; defaults to '/auth/login'
25
+ headerOffset?: 'auto' | 'none' | number; // default 'auto' (measure <header>), use 'none' when no header
23
26
  };
24
27
 
25
- export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId }: Props) {
28
+ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId, loginPath = '/auth/login', headerOffset = 'auto' }: Props) {
26
29
  // Sidebar open state (default expanded)
27
30
  const [open, setOpen] = useState<boolean>(() => {
28
31
  try { const s = localStorage.getItem('kydChatSidebarOpen'); return s ? s === '1' : true; } catch { return true; }
@@ -46,7 +49,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
46
49
  const tabDraggedRef = useRef(false);
47
50
  const [headerTop, setHeaderTop] = useState(0);
48
51
  const [showHint, setShowHint] = useState(false);
49
- const { messages, input, setInput, sending, sendMessage, cancel } = useChatStreaming({ api, badgeId });
52
+ const { messages, input, setInput, sending, sendMessage, cancel } = useChatStreaming({ api, badgeId, loginPath });
50
53
  const listRef = useRef<HTMLDivElement>(null);
51
54
 
52
55
  useEffect(() => {
@@ -117,13 +120,23 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
117
120
  return () => window.removeEventListener('keydown', onKey);
118
121
  }, [open]);
119
122
 
120
- // Measure sticky header height so the sidebar does not overlap it
123
+ // Header offset handling to keep chat below any sticky header in host app
121
124
  useEffect(() => {
125
+ if (headerOffset === 'none') {
126
+ setHeaderTop(0);
127
+ setTabTop(t => Math.max(16, t));
128
+ return;
129
+ }
130
+ if (typeof headerOffset === 'number' && Number.isFinite(headerOffset)) {
131
+ const h = Math.max(0, Math.floor(headerOffset));
132
+ setHeaderTop(h);
133
+ setTabTop(t => Math.max(h + 16, t));
134
+ return;
135
+ }
122
136
  const measure = () => {
123
137
  const el = document.querySelector('header');
124
138
  const h = el ? Math.floor(el.getBoundingClientRect().height) : 0;
125
139
  setHeaderTop(h);
126
- // Keep collapsed tab within bounds beneath header
127
140
  setTabTop(t => Math.max(h + 16, t));
128
141
  };
129
142
  measure();
@@ -133,7 +146,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
133
146
  window.removeEventListener('resize', measure);
134
147
  window.removeEventListener('scroll', measure);
135
148
  };
136
- }, []);
149
+ }, [headerOffset]);
137
150
 
138
151
  // Drag to resize (left edge of sidebar)
139
152
  useEffect(() => {
@@ -8,10 +8,12 @@ export type ChatMessage = { id: string; role: Role; content: string };
8
8
  export type UseChatStreamingConfig = {
9
9
  api: string; // e.g. /api/chat
10
10
  badgeId: string;
11
+ // Optional login path for redirect when 401 occurs. Defaults to '/auth/login' for portability across apps.
12
+ loginPath?: string;
11
13
  };
12
14
 
13
15
  export function useChatStreaming(cfg: UseChatStreamingConfig) {
14
- const { api, badgeId } = cfg;
16
+ const { api, badgeId, loginPath = '/auth/login' } = cfg;
15
17
  const [messages, setMessages] = useState<ChatMessage[]>([]);
16
18
  const [input, setInput] = useState('');
17
19
  const [sending, setSending] = useState(false);
@@ -32,7 +34,8 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
32
34
  // Inform the user then redirect to login carrying callbackUrl and prompt
33
35
  setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, content: 'You need to log in to use chat. Redirecting to login…' } : msg));
34
36
  const currentPath = typeof window !== 'undefined' ? (window.location.pathname + window.location.search + window.location.hash) : '/';
35
- const loginUrl = `/auth/login?callbackUrl=${encodeURIComponent(currentPath)}&chatPrompt=${encodeURIComponent(promptText)}`;
37
+ const base = loginPath || '/auth/login';
38
+ const loginUrl = `${base}?callbackUrl=${encodeURIComponent(currentPath)}&chatPrompt=${encodeURIComponent(promptText)}`;
36
39
  setTimeout(() => {
37
40
  if (typeof window !== 'undefined') window.location.href = loginUrl;
38
41
  }, 700);
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">