kyd-shared-badge 0.3.65 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.65",
3
+ "version": "0.3.66",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -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
- return (parsedSources as (BusinessRuleRow & { pillar?: string })[]).slice().sort((a, b) => {
219
- const ap = (a as any).pillar || '';
220
- const bp = (b as any).pillar || '';
221
- const cmpPillar = orderForPillar(ap) - orderForPillar(bp);
222
- if (cmpPillar !== 0) return cmpPillar;
223
- const ac = (a.category || '').localeCompare(b.category || '');
224
- if (ac !== 0) return ac;
225
- // Within same category, sort by descending weight for usefulness
226
- return Number(b.weight || 0) - Number(a.weight || 0);
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
- <th key={header} scope="col" className={'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider'} style={{ color: 'var(--text-secondary)' }}>
256
- {header}
257
- </th>
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 id={anchorId} key={index} className={'transition-colors hover:bg-black/5'}>
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>
package/src/types.ts CHANGED
@@ -9,6 +9,10 @@ export type Provider = {
9
9
  hoverBg: string;
10
10
  text: string;
11
11
  };
12
+ endpoint?: string;
13
+ placeholder?: string;
14
+ copy?: string;
15
+ beta?: boolean;
12
16
  };
13
17
 
14
18
  export interface PublicBadgeData {