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.
- package/package.json +1 -1
- package/src/PrintableBadgeDisplay.tsx +608 -0
- package/src/SharedBadgeDisplay.tsx +514 -396
- package/src/components/CategoryBars.tsx +45 -2
- package/src/components/Skills.tsx +13 -4
- package/src/components/SkillsAppendixTable.tsx +65 -42
- package/src/index.ts +1 -0
- package/src/types.ts +3 -0
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
<div className={'
|
|
64
|
-
|
|
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
|
-
</
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
</
|
|
75
|
-
</
|
|
76
|
-
|
|
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 {
|