kyd-shared-badge 0.2.26 → 0.2.28

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.
@@ -0,0 +1,422 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { green } from '../colors';
5
+ import { SkillsMatrix } from '../types';
6
+ import {
7
+ ResponsiveContainer,
8
+ BarChart,
9
+ Bar,
10
+ XAxis,
11
+ YAxis,
12
+ CartesianGrid,
13
+ Tooltip,
14
+ } from 'recharts';
15
+
16
+
17
+ type SkillsRadarPoint = {
18
+ axis: string;
19
+ observed?: number;
20
+ self_reported?: number;
21
+ certified?: number;
22
+ };
23
+
24
+
25
+ type BubbleDatum = { label: string; value: number; color: string };
26
+ type PackedBubble = { x: number; y: number; r: number; label: string; value: number; color: string };
27
+
28
+ const packBubbles = (data: BubbleDatum[], width: number, height: number, padding: number): PackedBubble[] => {
29
+ const filtered = data.filter((d) => Number(d.value) > 0);
30
+ if (filtered.length === 0) return [];
31
+ const maxVal = Math.max(...filtered.map((d) => d.value));
32
+ const maxR = Math.min(width, height) * 0.24;
33
+ const nodes: PackedBubble[] = filtered
34
+ .sort((a, b) => b.value - a.value)
35
+ .map((d) => ({
36
+ label: d.label,
37
+ value: d.value,
38
+ color: d.color,
39
+ r: Math.max(6, Math.sqrt(d.value / maxVal) * maxR),
40
+ x: width / 2,
41
+ y: height / 2,
42
+ }));
43
+
44
+ const placed: PackedBubble[] = [];
45
+ const centerX = width / 2;
46
+ const centerY = height / 2;
47
+ for (let i = 0; i < nodes.length; i++) {
48
+ const node = nodes[i];
49
+ if (i === 0) {
50
+ placed.push({ ...node });
51
+ continue;
52
+ }
53
+ let angle = 0;
54
+ let radiusFromCenter = node.r + padding;
55
+ let found = false;
56
+ const maxAttempts = 2000;
57
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
58
+ const x = centerX + radiusFromCenter * Math.cos(angle);
59
+ const y = centerY + radiusFromCenter * Math.sin(angle);
60
+ const withinBounds = (x - node.r >= 0) && (x + node.r <= width) && (y - node.r >= 0) && (y + node.r <= height);
61
+ if (withinBounds) {
62
+ let overlaps = false;
63
+ for (let j = 0; j < placed.length; j++) {
64
+ const p = placed[j];
65
+ const dx = x - p.x;
66
+ const dy = y - p.y;
67
+ const dist2 = dx * dx + dy * dy;
68
+ const minDist = node.r + p.r + padding;
69
+ if (dist2 < minDist * minDist) {
70
+ overlaps = true;
71
+ break;
72
+ }
73
+ }
74
+ if (!overlaps) {
75
+ placed.push({ ...node, x, y });
76
+ found = true;
77
+ break;
78
+ }
79
+ }
80
+ angle += Math.PI / 18; // 10 degrees
81
+ if (angle >= Math.PI * 2) {
82
+ angle = 0;
83
+ radiusFromCenter += Math.max(4, node.r * 0.25);
84
+ }
85
+ }
86
+ if (!found) {
87
+ placed.push({ ...node, x: Math.min(Math.max(node.r, centerX), width - node.r), y: Math.min(Math.max(node.r, centerY), height - node.r) });
88
+ }
89
+ }
90
+ return placed;
91
+ };
92
+
93
+ // Compute up to two centered lines that fit inside a circle of given radius.
94
+ // Uses a rough char width approximation to avoid expensive text measurement.
95
+ const buildLabelLines = (label: string, radius: number): { lines: string[]; fontSize: number } => {
96
+ const clean = String(label || '').trim();
97
+ if (!clean) return { lines: [], fontSize: 10 };
98
+
99
+ // Choose font size based on radius, within reasonable bounds
100
+ const fontSize = Math.max(8, Math.min(14, Math.floor(radius * 0.45)));
101
+ const charWidth = fontSize * 0.58; // approx width per character
102
+ const maxLineWidth = Math.max(0, (radius * 2) * 0.85);
103
+ const maxCharsPerLine = Math.max(2, Math.floor(maxLineWidth / charWidth));
104
+
105
+ // Prefer breaking on spaces; fallback to hard cut
106
+ const words = clean.split(/\s+/);
107
+ const lines: string[] = [];
108
+ let current = '';
109
+ for (let i = 0; i < words.length; i++) {
110
+ const w = words[i];
111
+ const candidate = current ? current + ' ' + w : w;
112
+ if (candidate.length <= maxCharsPerLine) {
113
+ current = candidate;
114
+ } else {
115
+ if (current) lines.push(current);
116
+ current = w.length > maxCharsPerLine ? w.slice(0, Math.max(1, maxCharsPerLine - 1)) + '…' : w;
117
+ }
118
+ if (lines.length >= 2) break;
119
+ }
120
+ if (lines.length < 2 && current) lines.push(current);
121
+
122
+ // If still too many words, force truncate to 2 lines
123
+ if (lines.length > 2) lines.length = 2;
124
+ // Ensure each line within limit
125
+ for (let i = 0; i < lines.length; i++) {
126
+ if (lines[i].length > maxCharsPerLine) {
127
+ lines[i] = lines[i].slice(0, Math.max(1, maxCharsPerLine - 1)) + '…';
128
+ }
129
+ }
130
+
131
+ return { lines, fontSize };
132
+ };
133
+
134
+ type HoverTooltipState = {
135
+ visible: boolean;
136
+ x: number;
137
+ y: number;
138
+ title: string;
139
+ body?: string;
140
+ } | null;
141
+
142
+ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
143
+ if (!state || !state.visible) return null;
144
+ return (
145
+ <div
146
+ style={{
147
+ position: 'absolute',
148
+ left: state.x,
149
+ top: state.y,
150
+ pointerEvents: 'none',
151
+ background: 'var(--content-card-background)',
152
+ border: '1px solid var(--icon-button-secondary)',
153
+ color: 'var(--text-main)',
154
+ padding: 10,
155
+ borderRadius: 6,
156
+ minWidth: 250,
157
+ maxWidth: 320,
158
+ zIndex: 10,
159
+ }}
160
+ >
161
+ <div style={{ fontWeight: 600 }}>{state.title}</div>
162
+ {state.body ? (
163
+ <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{state.body}</div>
164
+ ) : null}
165
+ </div>
166
+ );
167
+ };
168
+
169
+ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCategoryRadar?: SkillsRadarPoint[] }) => {
170
+ // const hasMatrix = !!(skillsMatrix && Array.isArray(skillsMatrix.skills) && skillsMatrix.skills.length > 0);
171
+ const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
172
+
173
+ const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 8);
174
+
175
+ const containerRef = useRef<HTMLDivElement>(null);
176
+ const footprintLegendRef = useRef<HTMLDivElement>(null);
177
+ const barLegendRef = useRef<HTMLDivElement>(null);
178
+ const [containerWidth, setContainerWidth] = useState<number>(0);
179
+ const [footprintChartTooltip, setFootprintChartTooltip] = useState<HoverTooltipState>(null);
180
+ const [footprintLegendTooltip, setFootprintLegendTooltip] = useState<HoverTooltipState>(null);
181
+ const [barLegendTooltip, setBarLegendTooltip] = useState<HoverTooltipState>(null);
182
+
183
+ useEffect(() => {
184
+ const measure = () => setContainerWidth(containerRef.current?.clientWidth || 0);
185
+ measure();
186
+ window.addEventListener('resize', measure);
187
+ return () => window.removeEventListener('resize', measure);
188
+ }, []);
189
+
190
+ const combinedBubbleData: BubbleDatum[] = useMemo(() => {
191
+ return skillsRadarLimited.map((d) => {
192
+ const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
193
+ const nonZero = vals.filter((v) => v > 0);
194
+ const base = (nonZero.length > 0 ? nonZero : vals);
195
+ const avg = Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1));
196
+ return { label: d.axis, value: avg, color: green };
197
+ });
198
+ }, [skillsRadarLimited]);
199
+
200
+ const legendData = useMemo(() => {
201
+ const total = combinedBubbleData.reduce((sum, d) => sum + d.value, 0);
202
+ return combinedBubbleData
203
+ .slice()
204
+ .sort((a, b) => b.value - a.value)
205
+ .map((d) => ({ label: d.label, percent: total > 0 ? Math.round((d.value / total) * 100) : 0 }));
206
+ }, [combinedBubbleData]);
207
+
208
+ return (
209
+ <div className="space-y-6">
210
+
211
+ {/* Skills Coverage and Breakdown: two charts side-by-side */}
212
+ {hasRadar && (
213
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
214
+ <div className={'rounded-lg p-4 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
215
+ <h4 className={'font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
216
+ <p className={'text-sm mb-4'} style={{ color: 'var(--text-secondary)' }}>The bubble chart visualizes individual skills, where bubble size reflects the weight of supporting evidence and placement indicates relative strength across the skill set.</p>
217
+ <div ref={containerRef} style={{ width: '100%', height: 340, position: 'relative' }}>
218
+ {(() => {
219
+ const width = containerWidth || 600;
220
+ const height = 300;
221
+ const innerWidth = Math.max(0, width);
222
+ const innerHeight = Math.max(0, height);
223
+ const packed = packBubbles(combinedBubbleData, innerWidth, innerHeight, 6);
224
+ return (
225
+ <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
226
+ <g transform={`translate(-25, -20)`}>
227
+ {packed.map((b, idx) => {
228
+ const clipId = `bubble-clip-${idx}`;
229
+ const canShowText = b.r >= 14;
230
+ const labelSpec = canShowText ? buildLabelLines(b.label, b.r) : { lines: [], fontSize: 10 };
231
+ const lineGap = Math.max(2, Math.floor(labelSpec.fontSize * 0.2));
232
+ const totalTextHeight = labelSpec.lines.length * labelSpec.fontSize + Math.max(0, (labelSpec.lines.length - 1) * lineGap);
233
+ const startY = -Math.floor(totalTextHeight / 2) + Math.floor(labelSpec.fontSize * 0.85);
234
+ return (
235
+ <g
236
+ key={idx}
237
+ transform={`translate(${b.x},${b.y})`}
238
+ onMouseEnter={(e) => {
239
+ const rect = containerRef.current?.getBoundingClientRect();
240
+ if (!rect) return;
241
+ setFootprintChartTooltip({
242
+ visible: true,
243
+ x: e.clientX - rect.left + 12,
244
+ y: e.clientY - rect.top + 12,
245
+ title: b.label,
246
+ body: `Score: ${b.value}`,
247
+ });
248
+ }}
249
+ onMouseMove={(e) => {
250
+ if (!footprintChartTooltip || !containerRef.current) return;
251
+ const rect = containerRef.current.getBoundingClientRect();
252
+ setFootprintChartTooltip({ ...footprintChartTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
253
+ }}
254
+ onMouseLeave={() => setFootprintChartTooltip(null)}
255
+ >
256
+ <defs>
257
+ <clipPath id={clipId}>
258
+ <circle r={b.r} />
259
+ </clipPath>
260
+ </defs>
261
+ <circle r={b.r} fill={'var(--bubble-foreground)'} stroke={'var(--icon-button-secondary)'} />
262
+ <title>{`${b.label}: ${b.value}`}</title>
263
+ {canShowText && labelSpec.lines.length > 0 ? (
264
+ <g clipPath={`url(#${clipId})`} style={{ pointerEvents: 'none' }}>
265
+ {labelSpec.lines.map((line, li) => (
266
+ <text key={li} y={startY + li * (labelSpec.fontSize + lineGap)} fontSize={labelSpec.fontSize} textAnchor="middle" fill={'var(--bubble-background)'}>
267
+ {line}
268
+ </text>
269
+ ))}
270
+ </g>
271
+ ) : null}
272
+ </g>
273
+ );
274
+ })}
275
+ </g>
276
+ </svg>
277
+ );
278
+ })()}
279
+ <TooltipBox state={footprintChartTooltip} />
280
+ </div>
281
+ {/* Legend */}
282
+ <div className={'mt-3'}>
283
+ {/* <div className={'text-xs mb-2'} style={{ color: 'var(--text-secondary)' }}>Legend</div> */}
284
+ <div ref={footprintLegendRef} style={{ position: 'relative' }}>
285
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
286
+ {legendData.map((item, idx) => (
287
+ <div
288
+ key={idx}
289
+ className="flex items-center gap-2 text-xs"
290
+ style={{ color: 'var(--text-secondary)' }}
291
+ onMouseEnter={(e) => {
292
+ const rect = footprintLegendRef.current?.getBoundingClientRect();
293
+ if (!rect) return;
294
+ const x = e.clientX - rect.left + 12;
295
+ const y = e.clientY - rect.top + 12;
296
+ setFootprintLegendTooltip({
297
+ visible: true,
298
+ x,
299
+ y,
300
+ title: item.label,
301
+ body: `${item.label} represents ${item.percent}% of the Developer’s observable technical skills.`,
302
+ });
303
+ }}
304
+ onMouseMove={(e) => {
305
+ if (!footprintLegendTooltip || !footprintLegendRef.current) return;
306
+ const rect = footprintLegendRef.current.getBoundingClientRect();
307
+ setFootprintLegendTooltip({ ...footprintLegendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
308
+ }}
309
+ onMouseLeave={() => setFootprintLegendTooltip(null)}
310
+ >
311
+ <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--text-secondary)', flexShrink: 0 }} />
312
+ <span className="truncate">{item.label}</span>
313
+ <span className="ml-auto opacity-80">{item.percent}%</span>
314
+ </div>
315
+ ))}
316
+ </div>
317
+ <TooltipBox state={footprintLegendTooltip} />
318
+ </div>
319
+ </div>
320
+ </div>
321
+ <div className={'rounded-lg p-4 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
322
+ <h4 className={'font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills by Validation Type</h4>
323
+ <p className={'text-sm mb-4'} style={{ color: 'var(--text-secondary)' }}>The bar chart shows how each skill is supported by self-attested claims, observed practice, or certified evidence.</p>
324
+ <div className="space-y-6 flex-col items-center" >
325
+ <div style={{ height: 360 }} >
326
+ <div className="flex-1 min-h-0 h-full">
327
+ <ResponsiveContainer>
328
+ <BarChart data={skillsRadarLimited} margin={{ top: 8, right: 8, left: 8, bottom: 36 }}>
329
+ <CartesianGrid strokeDasharray="3 3" stroke={'var(--icon-button-secondary)'} />
330
+ <XAxis dataKey="axis" tick={{ fill: 'var(--text-secondary)', fontSize: 12 }} interval={0} angle={-20} textAnchor="end" height={50} />
331
+ <YAxis domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} />
332
+ <Tooltip contentStyle={{ background: 'var(--content-card-background)', border: `1px solid var(--icon-button-secondary)`, color: 'var(--text-main)' }} />
333
+ <Bar dataKey="observed" name="Observed" fill={'var(--bar-observed)'} />
334
+ <Bar dataKey="self_reported" name="Self-reported" fill={'var(--bar-self-reported)'} />
335
+ <Bar dataKey="certified" name="Certified" fill={'var(--bar-certified)'} />
336
+ </BarChart>
337
+ </ResponsiveContainer>
338
+ </div>
339
+ </div>
340
+ <div ref={barLegendRef} className="mt-2" style={{ position: 'relative' }}>
341
+ <div className="grid grid-cols-3 gap-3 text-xs" style={{ color: 'var(--text-secondary)' }}>
342
+ <div
343
+ className="flex items-center gap-2"
344
+ onMouseEnter={(e) => {
345
+ const rect = barLegendRef.current?.getBoundingClientRect();
346
+ if (!rect) return;
347
+ setBarLegendTooltip({
348
+ visible: true,
349
+ x: e.clientX - rect.left + 12,
350
+ y: e.clientY - rect.top + 12,
351
+ title: 'Observed',
352
+ body: 'Observed: Skills evidenced through third-party activity or recognition.',
353
+ });
354
+ }}
355
+ onMouseMove={(e) => {
356
+ if (!barLegendTooltip || !barLegendRef.current) return;
357
+ const rect = barLegendRef.current.getBoundingClientRect();
358
+ setBarLegendTooltip({ ...barLegendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
359
+ }}
360
+ onMouseLeave={() => setBarLegendTooltip(null)}
361
+ >
362
+ <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--bar-observed)' }} />
363
+ <span>Observed</span>
364
+ </div>
365
+ <div
366
+ className="flex items-center gap-2"
367
+ onMouseEnter={(e) => {
368
+ const rect = barLegendRef.current?.getBoundingClientRect();
369
+ if (!rect) return;
370
+ setBarLegendTooltip({
371
+ visible: true,
372
+ x: e.clientX - rect.left + 12,
373
+ y: e.clientY - rect.top + 12,
374
+ title: 'Self-Attested',
375
+ body: 'Self-Attested: Skills claimed directly by the developer.',
376
+ });
377
+ }}
378
+ onMouseMove={(e) => {
379
+ if (!barLegendTooltip || !barLegendRef.current) return;
380
+ const rect = barLegendRef.current.getBoundingClientRect();
381
+ setBarLegendTooltip({ ...barLegendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
382
+ }}
383
+ onMouseLeave={() => setBarLegendTooltip(null)}
384
+ >
385
+ <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--bar-self-reported)' }} />
386
+ <span>Self-reported</span>
387
+ </div>
388
+ <div
389
+ className="flex items-center gap-2"
390
+ onMouseEnter={(e) => {
391
+ const rect = barLegendRef.current?.getBoundingClientRect();
392
+ if (!rect) return;
393
+ setBarLegendTooltip({
394
+ visible: true,
395
+ x: e.clientX - rect.left + 12,
396
+ y: e.clientY - rect.top + 12,
397
+ title: 'Certified',
398
+ body: 'Certified: Skills verified by formal credentials or certifications',
399
+ });
400
+ }}
401
+ onMouseMove={(e) => {
402
+ if (!barLegendTooltip || !barLegendRef.current) return;
403
+ const rect = barLegendRef.current.getBoundingClientRect();
404
+ setBarLegendTooltip({ ...barLegendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
405
+ }}
406
+ onMouseLeave={() => setBarLegendTooltip(null)}
407
+ >
408
+ <span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--bar-certified)' }} />
409
+ <span>Certified</span>
410
+ </div>
411
+ </div>
412
+ <TooltipBox state={barLegendTooltip} />
413
+ </div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ )}
418
+ </div>
419
+ );
420
+ };
421
+
422
+ export default Skills;
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect } from 'react';
4
+ import { SkillsAll, SkillRow } from '../types';
5
+ import { green } from '../colors';
6
+
7
+ const slugify = (name: string) =>
8
+ String(name || '')
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, '-')
11
+ .replace(/^-+|-+$/g, '');
12
+
13
+ const SkillsAppendixTable = ({ skillsAll }: { skillsAll?: SkillsAll }) => {
14
+ const rows = (skillsAll?.skills || []).filter((row: SkillRow) => row.observed?.present || row.self_reported?.present || row.certified?.present);
15
+ useEffect(() => {
16
+ const flash = () => {
17
+ const hash = typeof window !== 'undefined' ? window.location.hash : '';
18
+ if (!hash || !hash.startsWith('#appendix-skills-')) return;
19
+ const id = hash.slice(1);
20
+ const el = document.getElementById(id) as HTMLElement | null;
21
+ if (!el) return;
22
+ const originalBg = el.style.backgroundColor;
23
+ el.style.transition = 'background-color 300ms ease';
24
+ el.style.backgroundColor = 'rgba(2, 163, 137, 0.14)';
25
+ setTimeout(() => {
26
+ el.style.backgroundColor = originalBg || '';
27
+ }, 1200);
28
+ };
29
+ flash();
30
+ window.addEventListener('hashchange', flash);
31
+ return () => window.removeEventListener('hashchange', flash);
32
+ }, []);
33
+
34
+ if (!rows.length) return null;
35
+ 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}
65
+ </div>
66
+ )}
67
+ </div>
68
+ </div>
69
+ </td>
70
+ );
71
+ })}
72
+ </tr>
73
+ ))}
74
+ </tbody>
75
+ </table>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default SkillsAppendixTable;
82
+
83
+