kyd-shared-badge 0.3.64 → 0.3.66
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/PrintableBadgeDisplay.tsx +0 -16
- package/src/SharedBadgeDisplay.tsx +2 -2
- package/src/components/AppendixTables.tsx +91 -18
- package/src/components/charts/ChartEmptyState.tsx +34 -0
- package/src/components/charts/ChartLegend.tsx +19 -0
- package/src/components/charts/ChartTooltip.tsx +80 -0
- package/src/index.ts +5 -1
- package/src/types.ts +4 -0
package/package.json
CHANGED
|
@@ -31,7 +31,6 @@ import AiUsageBody from './components/AiUsageBody';
|
|
|
31
31
|
import SanctionsMatches from './components/SanctionsMatches';
|
|
32
32
|
import AppendixContent from './components/AppendixContent';
|
|
33
33
|
import { ProviderIcon, getProviderDisplayName, getProviderTooltipCopy, getCategoryTooltipCopy, barColor } from './utils/provider';
|
|
34
|
-
import ResumeView from './components/ResumeView';
|
|
35
34
|
type ChatWidgetProps = Partial<{
|
|
36
35
|
api: string;
|
|
37
36
|
title: string;
|
|
@@ -224,21 +223,6 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
224
223
|
|
|
225
224
|
|
|
226
225
|
<div className="pt-8 space-y-8">
|
|
227
|
-
{(() => {
|
|
228
|
-
const resumeJson = (assessmentResult as any)?.resume?.json || null;
|
|
229
|
-
const roleName = (() => {
|
|
230
|
-
try { return (assessmentResult?.enterprise_match?.role?.name) || undefined; } catch { return undefined; }
|
|
231
|
-
})();
|
|
232
|
-
if (!resumeJson) return null;
|
|
233
|
-
return (
|
|
234
|
-
<Reveal headless={isHeadless}>
|
|
235
|
-
<div className={'kyd-avoid-break'}>
|
|
236
|
-
<h3 className={'text-2xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>Resume</h3>
|
|
237
|
-
<ResumeView resume={resumeJson} roleName={roleName} showDownloadButton={false} />
|
|
238
|
-
</div>
|
|
239
|
-
</Reveal>
|
|
240
|
-
);
|
|
241
|
-
})()}
|
|
242
226
|
<Reveal headless={isHeadless} as={'h3'} offsetY={8} className={`text-2xl font-bold ${isHeadless ? 'kyd-break-before' : ''}`} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</Reveal>
|
|
243
227
|
|
|
244
228
|
{/* Risk Graph Insights and Category Bars */}
|
|
@@ -564,11 +564,11 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
564
564
|
<TabNav />
|
|
565
565
|
<div className={'px-2 sm:px-0 pb-2'}>
|
|
566
566
|
{activeTab === 'overview' && OverviewSection()}
|
|
567
|
-
{activeTab === 'resume' && <ResumeSection />}
|
|
568
|
-
{activeTab === 'role' && <RoleFitSection />}
|
|
569
567
|
{activeTab === 'technical' && TechnicalSection()}
|
|
570
568
|
{activeTab === 'risk' && RiskSection()}
|
|
571
569
|
{activeTab === 'ai' && AiSection()}
|
|
570
|
+
{activeTab === 'role' && <RoleFitSection />}
|
|
571
|
+
{activeTab === 'resume' && <ResumeSection />}
|
|
572
572
|
{activeTab === 'appendix' && AppendixSection()}
|
|
573
573
|
</div>
|
|
574
574
|
</>
|
|
@@ -5,6 +5,10 @@ import { formatLocalDate } from '../utils/date';
|
|
|
5
5
|
import { FaGithub, FaGitlab, FaStackOverflow, FaLinkedin, FaGoogle, FaKaggle } from 'react-icons/fa';
|
|
6
6
|
import { SiCredly, SiFiverr } from 'react-icons/si';
|
|
7
7
|
import { DomainCSVRow } from '../types';
|
|
8
|
+
import { red, yellow, green, hexToRgba } from '../colors';
|
|
9
|
+
import RiskIcon from './icons/risk';
|
|
10
|
+
import CodeIcon from './icons/code';
|
|
11
|
+
import AiIcon from './icons/ai';
|
|
8
12
|
|
|
9
13
|
interface SanctionSource {
|
|
10
14
|
issuingEntity: string;
|
|
@@ -134,6 +138,8 @@ const DomainRow = ({ source, searchedAt, developerName }: { source: DomainSource
|
|
|
134
138
|
const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedAt, developerName, genreMapping, headless }) => {
|
|
135
139
|
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
|
|
136
140
|
const [expanded, setExpanded] = useState<{ [k: number]: boolean }>({});
|
|
141
|
+
const [sortBy, setSortBy] = useState<'pillar' | 'category' | 'label' | 'weight' | null>(null);
|
|
142
|
+
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
|
137
143
|
|
|
138
144
|
useEffect(() => {
|
|
139
145
|
const flashIfRule = () => {
|
|
@@ -168,6 +174,27 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
168
174
|
? ["Country", "Entity Type", "Entity/Domain", "Searched On", "Value", "Findings"]
|
|
169
175
|
: ["Platform", "KYD Pillar", "Category", "Observation", "Weight Impact"];
|
|
170
176
|
|
|
177
|
+
const sortableBusinessHeaders: Record<string, 'pillar' | 'category' | 'label' | 'weight'> = {
|
|
178
|
+
'KYD Pillar': 'pillar',
|
|
179
|
+
'Category': 'category',
|
|
180
|
+
'Observation': 'label',
|
|
181
|
+
'Weight Impact': 'weight',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const onHeaderClick = (header: string) => {
|
|
185
|
+
if (type !== 'business_rules') return;
|
|
186
|
+
const key = sortableBusinessHeaders[header as keyof typeof sortableBusinessHeaders];
|
|
187
|
+
if (!key) return;
|
|
188
|
+
setSortBy(prev => {
|
|
189
|
+
if (prev === key) {
|
|
190
|
+
setSortDir(d => (d === 'asc' ? 'desc' : 'asc'));
|
|
191
|
+
return prev;
|
|
192
|
+
}
|
|
193
|
+
setSortDir('asc');
|
|
194
|
+
return key;
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
171
198
|
// Build quick lookup: category -> KYD Pillar (e.g., Technical or Risk)
|
|
172
199
|
const categoryToPillar: Record<string, string> = (() => {
|
|
173
200
|
const map: Record<string, string> = {};
|
|
@@ -214,16 +241,34 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
214
241
|
? (parsedSources as SanctionSource[]).slice().sort((a, b) => (b.matched === true ? 1 : 0) - (a.matched === true ? 1 : 0))
|
|
215
242
|
: type === 'business_rules'
|
|
216
243
|
? (() => {
|
|
217
|
-
const orderForPillar = (p: string) => (p === 'Technical' ? 0 : p === 'Risk' ? 1 : 2);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
const orderForPillar = (p: string) => (p === 'Technical' ? 0 : p === 'Risk' ? 1 : p === 'AI' ? 2 : 3);
|
|
245
|
+
const items = (parsedSources as (BusinessRuleRow & { pillar?: string })[]).slice();
|
|
246
|
+
if (!sortBy) {
|
|
247
|
+
return items.sort((a, b) => {
|
|
248
|
+
const ap = (a as any).pillar || '';
|
|
249
|
+
const bp = (b as any).pillar || '';
|
|
250
|
+
const cmpPillar = orderForPillar(ap) - orderForPillar(bp);
|
|
251
|
+
if (cmpPillar !== 0) return cmpPillar;
|
|
252
|
+
const ac = (a.category || '').localeCompare(b.category || '');
|
|
253
|
+
if (ac !== 0) return ac;
|
|
254
|
+
return Number(b.weight || 0) - Number(a.weight || 0);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return items.sort((a, b) => {
|
|
258
|
+
const dir = sortDir === 'asc' ? 1 : -1;
|
|
259
|
+
if (sortBy === 'pillar') {
|
|
260
|
+
return dir * (orderForPillar((a as any).pillar || '') - orderForPillar((b as any).pillar || ''));
|
|
261
|
+
}
|
|
262
|
+
if (sortBy === 'category') {
|
|
263
|
+
return dir * ((a.category || '').localeCompare(b.category || ''));
|
|
264
|
+
}
|
|
265
|
+
if (sortBy === 'label') {
|
|
266
|
+
return dir * ((a.label || '').localeCompare(b.label || ''));
|
|
267
|
+
}
|
|
268
|
+
if (sortBy === 'weight') {
|
|
269
|
+
return dir * (Number(a.weight || 0) - Number(b.weight || 0));
|
|
270
|
+
}
|
|
271
|
+
return 0;
|
|
227
272
|
});
|
|
228
273
|
})()
|
|
229
274
|
: parsedSources;
|
|
@@ -251,11 +296,23 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
251
296
|
</colgroup>
|
|
252
297
|
<thead className={''}>
|
|
253
298
|
<tr>
|
|
254
|
-
{headers.map(header =>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
299
|
+
{headers.map(header => {
|
|
300
|
+
const isSortable = type === 'business_rules' && Object.prototype.hasOwnProperty.call(sortableBusinessHeaders, header);
|
|
301
|
+
const isActive = isSortable && sortBy === sortableBusinessHeaders[header as keyof typeof sortableBusinessHeaders];
|
|
302
|
+
const arrow = isActive ? (sortDir === 'asc' ? ' ▲' : ' ▼') : '';
|
|
303
|
+
return (
|
|
304
|
+
<th
|
|
305
|
+
key={header}
|
|
306
|
+
scope="col"
|
|
307
|
+
onClick={() => onHeaderClick(header)}
|
|
308
|
+
className={'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider'}
|
|
309
|
+
style={{ color: 'var(--text-secondary)', cursor: isSortable ? 'pointer' : 'default', userSelect: isSortable ? 'none' : 'auto' }}
|
|
310
|
+
aria-sort={isActive ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
|
|
311
|
+
>
|
|
312
|
+
{header}{arrow}
|
|
313
|
+
</th>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
259
316
|
</tr>
|
|
260
317
|
</thead>
|
|
261
318
|
<tbody className={''}>
|
|
@@ -280,7 +337,7 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
280
337
|
}
|
|
281
338
|
const br = source as BusinessRuleRow & { pillar?: string };
|
|
282
339
|
const anchorId = br.uid ? `rule-${br.uid}` : undefined;
|
|
283
|
-
const ProviderIcon = ({ name }: { name?: string }) => {
|
|
340
|
+
const ProviderIcon = ({ name, pillar }: { name?: string, pillar?: string }) => {
|
|
284
341
|
const n = (name || '').toLowerCase();
|
|
285
342
|
if (n.includes('github')) return <FaGithub />;
|
|
286
343
|
if (n.includes('gitlab')) return <FaGitlab />;
|
|
@@ -290,13 +347,29 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
290
347
|
if (n.includes('kaggle')) return <FaKaggle />;
|
|
291
348
|
if (n.includes('google')) return <FaGoogle />;
|
|
292
349
|
if (n.includes('linkedin')) return <FaLinkedin />;
|
|
350
|
+
const p = (pillar || '').toLowerCase();
|
|
351
|
+
if (p === 'risk') return <RiskIcon width={28} height={28} />;
|
|
352
|
+
if (p === 'technical') return <CodeIcon width={28} height={28} />;
|
|
353
|
+
if (p === 'ai') return <AiIcon width={28} height={28} />;
|
|
293
354
|
return <span className="inline-block w-8 h-8 rounded-full" style={{ backgroundColor: 'var(--icon-button-secondary)' }} />;
|
|
294
355
|
};
|
|
295
356
|
return (
|
|
296
|
-
<tr
|
|
357
|
+
<tr
|
|
358
|
+
id={anchorId}
|
|
359
|
+
key={index}
|
|
360
|
+
className={'transition-colors hover:bg-black/5'}
|
|
361
|
+
style={{
|
|
362
|
+
backgroundColor: (() => {
|
|
363
|
+
const w = Number(br.weight);
|
|
364
|
+
if (!Number.isFinite(w)) return undefined;
|
|
365
|
+
const hex = w > 0 ? green : w < 0 ? red : yellow;
|
|
366
|
+
return hexToRgba(hex, 0.06);
|
|
367
|
+
})(),
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
297
370
|
<td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
298
371
|
<span title={br.provider || ''} className={'inline-flex items-center text-4xl'} style={{ color: 'var(--text-main)' }}>
|
|
299
|
-
<ProviderIcon name={br.provider} />
|
|
372
|
+
<ProviderIcon name={br.provider} pillar={br.pillar} />
|
|
300
373
|
</span>
|
|
301
374
|
</td>
|
|
302
375
|
<td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>{br.pillar || '—'}</td>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
export default function ChartEmptyState({ message }: { message: string }) {
|
|
6
|
+
// const width = 640
|
|
7
|
+
// const height = 180
|
|
8
|
+
// const padding = 28
|
|
9
|
+
// const gridColor = 'var(--icon-button-secondary)'
|
|
10
|
+
// const lineColor = 'var(--icon-button-secondary)'
|
|
11
|
+
// const areaColor = '#9CA3AF' // neutral-400-ish
|
|
12
|
+
|
|
13
|
+
// // A simple placeholder path
|
|
14
|
+
// const path = `M ${padding} ${height - padding - 20}
|
|
15
|
+
// C ${padding + 80} ${height - padding - 60},
|
|
16
|
+
// ${padding + 160} ${height - padding - 10},
|
|
17
|
+
// ${padding + 240} ${height - padding - 50}
|
|
18
|
+
// S ${padding + 400} ${height - padding - 30},
|
|
19
|
+
// ${width - padding} ${height - padding - 70}
|
|
20
|
+
// L ${width - padding} ${height - padding}
|
|
21
|
+
// L ${padding} ${height - padding} Z`
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="h-44 relative">
|
|
25
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
26
|
+
<div className="px-3 py-1.5 rounded-md border sm:text-sm text-xs" style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', color: 'var(--text-secondary)'}}>
|
|
27
|
+
{message}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
export default function ChartLegend({ items }: { items: Array<{ label: string; color: string }> }) {
|
|
6
|
+
if (!items?.length) return null
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs mb-2" style={{ color: 'var(--text-secondary)'}}>
|
|
9
|
+
{items.map((it) => (
|
|
10
|
+
<div key={it.label} className="flex items-center gap-1.5">
|
|
11
|
+
<span className="inline-block w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: it.color }} />
|
|
12
|
+
<span>{it.label}</span>
|
|
13
|
+
</div>
|
|
14
|
+
))}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
// type PayloadItem = {
|
|
6
|
+
// color?: string
|
|
7
|
+
// name?: string | number
|
|
8
|
+
// value?: number | string
|
|
9
|
+
// dataKey?: string
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
export default function ChartTooltip(props: any) {
|
|
14
|
+
const {
|
|
15
|
+
active,
|
|
16
|
+
label,
|
|
17
|
+
payload,
|
|
18
|
+
title,
|
|
19
|
+
valueLabel,
|
|
20
|
+
valueSuffix,
|
|
21
|
+
valueFormatter,
|
|
22
|
+
labelFormatter,
|
|
23
|
+
} = props || {}
|
|
24
|
+
if (!active || !payload || !payload.length) return null
|
|
25
|
+
|
|
26
|
+
const fmtLabel = () => {
|
|
27
|
+
try {
|
|
28
|
+
if (labelFormatter) return labelFormatter(label)
|
|
29
|
+
return new Date(String(label)).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
30
|
+
} catch {
|
|
31
|
+
return String(label)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fmtValue = (v: number | string) => {
|
|
36
|
+
const base = valueFormatter ? valueFormatter(v) : String(v)
|
|
37
|
+
return valueSuffix ? `${base}${valueSuffix}` : base
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className="rounded-md border shadow-sm px-3 py-2 text-xs"
|
|
43
|
+
style={{
|
|
44
|
+
background: 'var(--content-card-background)',
|
|
45
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
46
|
+
color: 'var(--text-main)'
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<div className="mb-1">
|
|
50
|
+
{title ? (
|
|
51
|
+
<div className="font-medium" style={{ color: 'var(--text-main)'}}>{title}</div>
|
|
52
|
+
) : null}
|
|
53
|
+
<div style={{ color: 'var(--text-secondary)' }}>{fmtLabel()}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-1">
|
|
56
|
+
{payload.map((item: { color?: string, name?: string | number, value?: number | string, dataKey?: string }, idx: number) => (
|
|
57
|
+
<div key={idx} className="flex items-center justify-between gap-3">
|
|
58
|
+
<div className="flex items-center gap-2">
|
|
59
|
+
<span className="inline-block w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: item.color || 'var(--icon-accent)' }} />
|
|
60
|
+
<span style={{ color: 'var(--text-secondary)'}}>{String(item.name || item.dataKey)}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="font-medium" style={{ color: 'var(--text-main)'}}>
|
|
63
|
+
{fmtValue(item.value ?? '')}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
{valueLabel && payload.length === 1 ? (
|
|
68
|
+
<div className="flex items-center justify-between gap-3">
|
|
69
|
+
<span style={{ color: 'var(--text-secondary)'}}>{valueLabel}</span>
|
|
70
|
+
<span className="font-medium" style={{ color: 'var(--text-main)'}}>
|
|
71
|
+
{fmtValue(payload[0].value ?? '')}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
package/src/index.ts
CHANGED
|
@@ -4,4 +4,8 @@ export { default as PrintableBadgeDisplay } from './PrintableBadgeDisplay';
|
|
|
4
4
|
export { default as ChatWindowStreaming } from './chat/ChatWindowStreaming';
|
|
5
5
|
export { default as ChatWidget } from './chat/ChatWidget';
|
|
6
6
|
export * from './utils/date';
|
|
7
|
-
export { getBadgeImageUrl } from './components/ReportHeader';
|
|
7
|
+
export { getBadgeImageUrl } from './components/ReportHeader';
|
|
8
|
+
|
|
9
|
+
export * from './components/charts/ChartTooltip';
|
|
10
|
+
export * from './components/charts/ChartEmptyState';
|
|
11
|
+
export * from './components/charts/ChartLegend';
|