kyd-shared-badge 0.3.35 → 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 +507 -399
- package/src/components/CategoryBars.tsx +15 -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 +2 -0
|
@@ -67,11 +67,21 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
|
|
|
67
67
|
const hasTarget = typeof targetRaw === 'number' && isFinite(targetRaw);
|
|
68
68
|
const targetClamp = hasTarget ? Math.max(0, Math.min(100, Math.round(targetRaw as number))) : 0;
|
|
69
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
|
+
};
|
|
70
80
|
return (
|
|
71
81
|
<div key={category} className="first:pt-0 group relative">
|
|
72
|
-
<
|
|
82
|
+
<button type="button" onClick={handleClick} className={'font-semibold mb-1 underline-offset-2 hover:underline text-left'} style={{ color: 'var(--text-main)' }}>
|
|
73
83
|
{category}
|
|
74
|
-
</
|
|
84
|
+
</button>
|
|
75
85
|
<div className="relative">
|
|
76
86
|
<div
|
|
77
87
|
className="w-full rounded-full overflow-hidden relative"
|
|
@@ -80,6 +90,9 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
|
|
|
80
90
|
background: 'transparent',
|
|
81
91
|
outline: '1px solid var(--icon-button-secondary)',
|
|
82
92
|
}}
|
|
93
|
+
onClick={handleClick}
|
|
94
|
+
role={'button'}
|
|
95
|
+
aria-label={`Jump to ${category} in Appendix`}
|
|
83
96
|
>
|
|
84
97
|
{/* signed fill originating from center */}
|
|
85
98
|
<div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ? `var(--status-negative, ${red})` : `var(--status-positive, ${green})` }} />
|
|
@@ -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