kyd-shared-badge 0.3.34 → 0.3.36

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.
@@ -11,6 +11,7 @@ type CategoryBarsProps = {
11
11
  barColor: (percent: number) => string;
12
12
  getCategoryTooltipCopy: (category: string) => string;
13
13
  barHeight?: number; // px height for the filled bar
14
+ categoryTargets?: Record<string, number>; // desired target lengths (0-100)
14
15
  };
15
16
 
16
17
  const CategoryBars: React.FC<CategoryBarsProps> = ({
@@ -19,12 +20,18 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
19
20
  categoryScores,
20
21
  getCategoryTooltipCopy,
21
22
  barHeight = 6,
23
+ categoryTargets,
22
24
  }) => {
23
25
  return (
24
26
  <div className="relative flex flex-col h-full">
25
27
  <div className="font-semibold text-xl mb-2" style={{ color: 'var(--text-main)' }}>{title}</div>
26
28
  <div className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
27
29
  Each bar represents a category's net evidence: positive values extend right in green, negative values extend left, and bar length denotes contribution magnitude.
30
+ {categoryTargets && Object.keys(categoryTargets).length > 0 ? (
31
+ <>
32
+ {' '}Target overlays indicate desired evidence lengths for this role.
33
+ </>
34
+ ) : null}
28
35
  </div>
29
36
  <div className="flex-1 flex flex-col justify-between relative">
30
37
  <div
@@ -56,11 +63,25 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
56
63
  'Strong Evidence';
57
64
  const fillWidth = absPercent / 2; // half-bar represents 100%
58
65
  const left = isNegative ? `calc(50% - ${fillWidth}%)` : '50%';
66
+ const targetRaw = categoryTargets?.[category];
67
+ const hasTarget = typeof targetRaw === 'number' && isFinite(targetRaw);
68
+ const targetClamp = hasTarget ? Math.max(0, Math.min(100, Math.round(targetRaw as number))) : 0;
69
+ const targetWidth = targetClamp / 2; // same scale as fillWidth
70
+ const handleClick = (e: React.MouseEvent) => {
71
+ // Navigate to Appendix -> Skills category anchor
72
+ try {
73
+ if (typeof window !== 'undefined') {
74
+ const anchor = `#appendix-skills-cat-${encodeURIComponent(category.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
75
+ const url = `#appendix${anchor}`;
76
+ window.location.hash = url;
77
+ }
78
+ } catch {}
79
+ };
59
80
  return (
60
81
  <div key={category} className="first:pt-0 group relative">
61
- <div className={'font-semibold mb-1'} style={{ color: 'var(--text-main)' }}>
82
+ <button type="button" onClick={handleClick} className={'font-semibold mb-1 underline-offset-2 hover:underline text-left'} style={{ color: 'var(--text-main)' }}>
62
83
  {category}
63
- </div>
84
+ </button>
64
85
  <div className="relative">
65
86
  <div
66
87
  className="w-full rounded-full overflow-hidden relative"
@@ -69,9 +90,26 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
69
90
  background: 'transparent',
70
91
  outline: '1px solid var(--icon-button-secondary)',
71
92
  }}
93
+ onClick={handleClick}
94
+ role={'button'}
95
+ aria-label={`Jump to ${category} in Appendix`}
72
96
  >
73
97
  {/* signed fill originating from center */}
74
98
  <div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ? `var(--status-negative, ${red})` : `var(--status-positive, ${green})` }} />
99
+ {/* role target overlay: positive side only, dashed outline */}
100
+ {hasTarget && (
101
+ <div
102
+ className="absolute top-0 h-full"
103
+ style={{
104
+ left: '50%',
105
+ width: `${targetWidth}%`,
106
+ borderTop: '2px dashed var(--icon-accent)',
107
+ borderBottom: '2px dashed var(--icon-accent)',
108
+ opacity: 0.7,
109
+ }}
110
+ aria-hidden="true"
111
+ />
112
+ )}
75
113
  </div>
76
114
  </div>
77
115
  <div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
@@ -89,6 +127,11 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
89
127
  <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
90
128
  {getCategoryTooltipCopy(category)}
91
129
  </div>
130
+ {hasTarget && (
131
+ <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
132
+ Target: {targetClamp}%
133
+ </div>
134
+ )}
92
135
  </div>
93
136
  </div>
94
137
  </div>
@@ -335,10 +335,19 @@ const Skills = ({ skillsCategoryRadar, headless }: { skillsMatrix?: SkillsMatrix
335
335
  <div ref={footprintLegendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
336
336
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
337
337
  {legendData.map((item, idx) => (
338
- <div
338
+ <button
339
339
  key={idx}
340
- className="flex items-center gap-2 text-xs"
341
- style={{ color: 'var(--text-secondary)' }}
340
+ className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
341
+ style={{ color: 'var(--text-secondary)', background: 'transparent' }}
342
+ onClick={() => {
343
+ try {
344
+ if (typeof window !== 'undefined') {
345
+ const anchor = `#appendix-skills-cat-${encodeURIComponent(item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
346
+ const url = `#appendix${anchor}`;
347
+ window.location.hash = url;
348
+ }
349
+ } catch {}
350
+ }}
342
351
  onMouseEnter={(e) => {
343
352
  const rect = footprintLegendRef.current?.getBoundingClientRect();
344
353
  if (!rect) return;
@@ -362,7 +371,7 @@ const Skills = ({ skillsCategoryRadar, headless }: { skillsMatrix?: SkillsMatrix
362
371
  <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--text-secondary)', flexShrink: 0 }} />
363
372
  <span className="truncate">{item.label}</span>
364
373
  <span className="ml-auto opacity-80">{item.percent}%</span>
365
- </div>
374
+ </button>
366
375
  ))}
367
376
  </div>
368
377
  {!headless && <TooltipBox state={footprintLegendTooltip} />}
@@ -15,8 +15,9 @@ const SkillsAppendixTable = ({ skillsAll }: { skillsAll?: SkillsAll }) => {
15
15
  useEffect(() => {
16
16
  const flash = () => {
17
17
  const hash = typeof window !== 'undefined' ? window.location.hash : '';
18
- if (!hash || !hash.startsWith('#appendix-skills-')) return;
19
- const id = hash.slice(1);
18
+ if (!hash) return;
19
+ // Support skill rows and category headers
20
+ const id = hash.startsWith('#') ? hash.slice(1) : hash;
20
21
  const el = document.getElementById(id) as HTMLElement | null;
21
22
  if (!el) return;
22
23
  const originalBg = el.style.backgroundColor;
@@ -32,48 +33,70 @@ const SkillsAppendixTable = ({ skillsAll }: { skillsAll?: SkillsAll }) => {
32
33
  }, []);
33
34
 
34
35
  if (!rows.length) return null;
36
+ // Group rows by category; skills can be in multiple categories
37
+ const groupMap: Record<string, SkillRow[]> = {};
38
+ const UNCATEGORIZED = 'Uncategorized';
39
+ for (const r of rows) {
40
+ const cats = Array.isArray((r as any).categories) && (r as any).categories.length > 0 ? (r as any).categories as string[] : [UNCATEGORIZED];
41
+ for (const c of cats) {
42
+ const key = c || UNCATEGORIZED;
43
+ if (!groupMap[key]) groupMap[key] = [];
44
+ groupMap[key].push(r);
45
+ }
46
+ }
47
+ const orderedCategories = Object.keys(groupMap).sort((a, b) => {
48
+ if (a === UNCATEGORIZED) return 1;
49
+ if (b === UNCATEGORIZED) return -1;
50
+ return a.localeCompare(b);
51
+ });
52
+
35
53
  return (
36
- <div id="appendix-skills" className="mt-4">
37
- <div className={'overflow-auto rounded-lg border'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
38
- <table className="min-w-full text-sm">
39
- <thead>
40
- <tr style={{ backgroundColor: 'var(--content-card-background)' }}>
41
- <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Skill</th>
42
- <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Observed</th>
43
- <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Self-reported</th>
44
- <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Certified</th>
45
- </tr>
46
- </thead>
47
- <tbody>
48
- {rows.map((row, idx: number) => (
49
- <tr id={`appendix-skills-${slugify(row.name)}`} key={idx} className="border-t" style={{ borderColor: 'var(--icon-button-secondary)' }}>
50
- <td className="p-3" style={{ color: 'var(--text-main)' }}>{row.name}</td>
51
- {(['observed','self_reported','certified'] as const).map((b) => {
52
- const bucket = row[b];
53
- const present = !!bucket.present;
54
- const dot = present ? green : 'var(--icon-button-secondary)';
55
- const ev = bucket.evidence;
56
- const sources = (bucket.sources || []).slice(0,4);
57
- return (
58
- <td key={b} className="p-3 align-top" style={{ color: 'var(--text-secondary)' }}>
59
- <div className="flex items-start gap-2">
60
- <div className={'mt-1 rounded-full'} style={{ backgroundColor: dot, minHeight: '8px', minWidth: '8px', height: '8px', width: '8px', flexShrink: 0 }} />
61
- <div>
62
- {(ev || (sources && sources.length > 0)) && (
63
- <div className={'text-xs mt-1'} style={{ color: 'var(--text-secondary)' }}>
64
- {ev ? <div>{ev}</div> : null}
54
+ <div id="appendix-skills" className="mt-4 space-y-8">
55
+ {orderedCategories.map((cat) => (
56
+ <div key={cat}>
57
+ <h5 id={`appendix-skills-cat-${slugify(cat)}`} className={'text-base font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>{cat}</h5>
58
+ <div className={'overflow-auto rounded-lg border'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
59
+ <table className="min-w-full text-sm">
60
+ <thead>
61
+ <tr style={{ backgroundColor: 'var(--content-card-background)' }}>
62
+ <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Skill</th>
63
+ <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Observed</th>
64
+ <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Self-reported</th>
65
+ <th className="text-left p-3" style={{ color: 'var(--text-secondary)' }}>Certified</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ {groupMap[cat].map((row, idx: number) => (
70
+ <tr id={`appendix-skills-${slugify(row.name)}`} key={`${cat}-${idx}`} className="border-t" style={{ borderColor: 'var(--icon-button-secondary)' }}>
71
+ <td className="p-3" style={{ color: 'var(--text-main)' }}>{row.name}</td>
72
+ {(['observed','self_reported','certified'] as const).map((b) => {
73
+ const bucket = row[b];
74
+ const present = !!bucket.present;
75
+ const dot = present ? green : 'var(--icon-button-secondary)';
76
+ const ev = bucket.evidence;
77
+ const sources = (bucket.sources || []).slice(0,4);
78
+ return (
79
+ <td key={b} className="p-3 align-top" style={{ color: 'var(--text-secondary)' }}>
80
+ <div className="flex items-start gap-2">
81
+ <div className={'mt-1 rounded-full'} style={{ backgroundColor: dot, minHeight: '8px', minWidth: '8px', height: '8px', width: '8px', flexShrink: 0 }} />
82
+ <div>
83
+ {(ev || (sources && sources.length > 0)) && (
84
+ <div className={'text-xs mt-1'} style={{ color: 'var(--text-secondary)' }}>
85
+ {ev ? <div>{ev}</div> : null}
86
+ </div>
87
+ )}
65
88
  </div>
66
- )}
67
- </div>
68
- </div>
69
- </td>
70
- );
71
- })}
72
- </tr>
73
- ))}
74
- </tbody>
75
- </table>
76
- </div>
89
+ </div>
90
+ </td>
91
+ );
92
+ })}
93
+ </tr>
94
+ ))}
95
+ </tbody>
96
+ </table>
97
+ </div>
98
+ </div>
99
+ ))}
77
100
  </div>
78
101
  );
79
102
  };
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export { default as SharedBadgeDisplay } from './SharedBadgeDisplay';
3
+ export { default as PrintableBadgeDisplay } from './PrintableBadgeDisplay';
3
4
  export { default as ChatWindowStreaming } from './chat/ChatWindowStreaming';
4
5
  export { default as ChatWidget } from './chat/ChatWidget';
5
6
  export * from './utils/date';
package/src/types.ts CHANGED
@@ -170,6 +170,7 @@ export interface EnterpriseMatch {
170
170
  role: {
171
171
  name: string;
172
172
  description: string;
173
+ categoryTargets?: Record<string, number>;
173
174
  };
174
175
  label: string;
175
176
  description: string;
@@ -415,6 +416,8 @@ export interface SkillRow {
415
416
  observed: SkillBucket;
416
417
  self_reported: SkillBucket;
417
418
  certified: SkillBucket;
419
+ // optional list of skill categories from backend
420
+ categories?: string[];
418
421
  }
419
422
 
420
423
  export interface SkillsMatrix {