kyd-shared-badge 0.3.22 → 0.3.24
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/SharedBadgeDisplay.tsx +32 -12
- package/src/components/AppendixTables.tsx +16 -10
- package/src/components/GraphInsights.tsx +32 -10
- package/src/components/Skills.tsx +32 -12
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ type ChatWidgetProps = Partial<{
|
|
|
40
40
|
// return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
41
41
|
// };
|
|
42
42
|
|
|
43
|
-
const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeData, chatProps?: ChatWidgetProps }) => {
|
|
43
|
+
const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: PublicBadgeData, chatProps?: ChatWidgetProps, headless?: boolean }) => {
|
|
44
44
|
const {
|
|
45
45
|
badgeId,
|
|
46
46
|
developerName,
|
|
@@ -61,6 +61,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
const wrapperMaxWidth = 'max-w-5xl';
|
|
64
|
+
const isHeadless = !!headless;
|
|
64
65
|
|
|
65
66
|
// Overall and genre scores
|
|
66
67
|
const overallFinalPercent = assessmentResult?.final_percent || 0;
|
|
@@ -145,6 +146,19 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
145
146
|
return (
|
|
146
147
|
<BusinessRulesProvider items={graphInsights?.business_rules_all}>
|
|
147
148
|
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
149
|
+
{isHeadless && (
|
|
150
|
+
<style>
|
|
151
|
+
{`@page { margin: 0; }
|
|
152
|
+
html, body { margin: 0 !important; padding: 0 !important; background: #fff !important; }
|
|
153
|
+
#__next, main { margin: 0 !important; padding: 0 !important; }
|
|
154
|
+
@media print {
|
|
155
|
+
.kyd-break-before { break-before: page; page-break-before: always; }
|
|
156
|
+
.kyd-break-after { break-after: page; page-break-after: always; }
|
|
157
|
+
.kyd-avoid-break { break-inside: avoid; page-break-inside: avoid; }
|
|
158
|
+
.kyd-keep-with-next { break-after: avoid; page-break-after: avoid; }
|
|
159
|
+
}`}
|
|
160
|
+
</style>
|
|
161
|
+
)}
|
|
148
162
|
{/* Share controls removed; app-level pages render their own actions */}
|
|
149
163
|
<Reveal offsetY={8} durationMs={500}>
|
|
150
164
|
<ReportHeader
|
|
@@ -165,14 +179,14 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
165
179
|
/>
|
|
166
180
|
</Reveal>
|
|
167
181
|
<div
|
|
168
|
-
className={'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
|
|
182
|
+
className={isHeadless ? 'p-6 sm:p-8 mt-2 border' : 'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
|
|
169
183
|
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
|
|
170
184
|
>
|
|
171
185
|
<div className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
172
|
-
<div className="pt-8 first:pt-0">
|
|
186
|
+
<div className="pt-8 first:pt-0 kyd-avoid-break">
|
|
173
187
|
<Reveal as={'h4'} offsetY={8} durationMs={500} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</Reveal>
|
|
174
188
|
<Reveal as={'div'} offsetY={8} durationMs={500} className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
175
|
-
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 *:min-h-full">
|
|
189
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 *:min-h-full kyd-avoid-break">
|
|
176
190
|
{/* Technical semicircle gauge (refactored) */}
|
|
177
191
|
{(() => {
|
|
178
192
|
const ui = graphInsights?.uiSummary?.technical || {};
|
|
@@ -236,7 +250,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
236
250
|
|
|
237
251
|
{/* Technical Scores */}
|
|
238
252
|
<div className="mt-8" >
|
|
239
|
-
<div key={'Technical'} className='pt-8 space-y-8' style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
253
|
+
<div key={'Technical'} className='pt-8 space-y-8 kyd-avoid-break' style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
240
254
|
<Reveal as={'h4'} offsetY={8} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical</Reveal>
|
|
241
255
|
{/* technical graph insights */}
|
|
242
256
|
<Reveal>
|
|
@@ -246,6 +260,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
246
260
|
categories={genreMapping?.['Technical'] as string[]}
|
|
247
261
|
genre={'Technical'}
|
|
248
262
|
scoringSummary={scoringSummary}
|
|
263
|
+
headless={isHeadless}
|
|
249
264
|
/>
|
|
250
265
|
</div>
|
|
251
266
|
</Reveal>
|
|
@@ -299,10 +314,10 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
299
314
|
</div>
|
|
300
315
|
|
|
301
316
|
<Reveal>
|
|
302
|
-
<div className="pt-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
303
|
-
<h3 className={'text-xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
317
|
+
<div className="pt-8 border-t kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
318
|
+
<h3 className={'text-xl font-bold mb-3 kyd-keep-with-next'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
304
319
|
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
305
|
-
<Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} />
|
|
320
|
+
<Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
306
321
|
</div>
|
|
307
322
|
</div>
|
|
308
323
|
</Reveal>
|
|
@@ -313,7 +328,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
313
328
|
|
|
314
329
|
|
|
315
330
|
<div className="pt-8 space-y-8">
|
|
316
|
-
<Reveal as={'h3'} offsetY={8} className={
|
|
331
|
+
<Reveal as={'h3'} offsetY={8} className={`text-2xl font-bold ${isHeadless ? 'kyd-break-before' : ''}`} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</Reveal>
|
|
317
332
|
|
|
318
333
|
{/* Risk Graph Insights and Category Bars */}
|
|
319
334
|
<Reveal>
|
|
@@ -323,10 +338,11 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
323
338
|
categories={genreMapping?.['Risk'] as string[]}
|
|
324
339
|
genre={'Risk'}
|
|
325
340
|
scoringSummary={scoringSummary}
|
|
341
|
+
headless={isHeadless}
|
|
326
342
|
/>
|
|
327
343
|
</div>
|
|
328
344
|
</Reveal>
|
|
329
|
-
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 w-full items-stretch py-8 border-y" style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
345
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 w-full items-stretch py-8 border-y kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
330
346
|
{/* Left: Bars */}
|
|
331
347
|
<Reveal className="lg:col-span-8 h-full">
|
|
332
348
|
<CategoryBars
|
|
@@ -473,7 +489,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
473
489
|
|
|
474
490
|
|
|
475
491
|
<div className="pt-8">
|
|
476
|
-
<h3 className={
|
|
492
|
+
<h3 className={`text-2xl font-bold mb-4 ${isHeadless ? 'kyd-break-before' : ''}`} style={{ color: 'var(--text-main)' }}>Appendix</h3>
|
|
477
493
|
<div className="space-y-8">
|
|
478
494
|
|
|
479
495
|
{/* Skills */}
|
|
@@ -499,6 +515,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
499
515
|
searchedAt={updatedAt}
|
|
500
516
|
developerName={developerName || 'this developer'}
|
|
501
517
|
genreMapping={genreMapping as Record<string, string[]>}
|
|
518
|
+
headless={isHeadless}
|
|
502
519
|
/>
|
|
503
520
|
</div>
|
|
504
521
|
</Reveal>
|
|
@@ -526,6 +543,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
526
543
|
sources={useDetailed ? detailed : dedup}
|
|
527
544
|
searchedAt={updatedAt}
|
|
528
545
|
developerName={developerName || 'this developer'}
|
|
546
|
+
headless={isHeadless}
|
|
529
547
|
/>
|
|
530
548
|
);
|
|
531
549
|
})()}
|
|
@@ -551,7 +569,9 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
551
569
|
</footer>
|
|
552
570
|
</Reveal>
|
|
553
571
|
{/* Floating chat widget */}
|
|
554
|
-
|
|
572
|
+
{!headless && (
|
|
573
|
+
<ChatWidget api={chatProps?.api || '/api/chat'} badgeId={badgeId} title={chatProps?.title} hintText={chatProps?.hintText} loginPath={chatProps?.loginPath} headerOffset={chatProps?.headerOffset} developerName={developerName} />
|
|
574
|
+
)}
|
|
555
575
|
</div>
|
|
556
576
|
</BusinessRulesProvider>
|
|
557
577
|
);
|
|
@@ -39,6 +39,7 @@ interface AppendixTableProps {
|
|
|
39
39
|
developerName: string;
|
|
40
40
|
// Used for business_rules: map genres (e.g., Technical/Risk) to category arrays
|
|
41
41
|
genreMapping?: Record<string, string[]>;
|
|
42
|
+
headless?: boolean;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const SanctionsRow = ({
|
|
@@ -48,6 +49,7 @@ const SanctionsRow = ({
|
|
|
48
49
|
onToggle,
|
|
49
50
|
expanded,
|
|
50
51
|
colSpan,
|
|
52
|
+
headless,
|
|
51
53
|
}: {
|
|
52
54
|
source: SanctionSource;
|
|
53
55
|
searchedAt: string;
|
|
@@ -55,13 +57,14 @@ const SanctionsRow = ({
|
|
|
55
57
|
onToggle: () => void;
|
|
56
58
|
expanded: boolean;
|
|
57
59
|
colSpan: number;
|
|
60
|
+
headless?: boolean;
|
|
58
61
|
}) => (
|
|
59
62
|
<>
|
|
60
63
|
<tr className={'transition-colors'} style={source.matched ? { backgroundColor: 'rgba(236,102,98,0.08)' } : undefined}>
|
|
61
64
|
<td className={'px-4 py-4 whitespace-nowrap align-top text-sm font-medium'} style={{ color: 'var(--text-main)' }}>
|
|
62
65
|
<div className="flex flex-col gap-1">
|
|
63
66
|
<span>{source.issuingEntity}</span>
|
|
64
|
-
{source.sublists && source.sublists.length > 0 && (
|
|
67
|
+
{source.sublists && source.sublists.length > 0 && !headless && (
|
|
65
68
|
<button
|
|
66
69
|
type="button"
|
|
67
70
|
onClick={onToggle}
|
|
@@ -94,7 +97,7 @@ const SanctionsRow = ({
|
|
|
94
97
|
: (<span>No exact match for <strong style={{ color: 'var(--text-main)' }}>{developerName}</strong> was found on this list.</span>)}
|
|
95
98
|
</td>
|
|
96
99
|
</tr>
|
|
97
|
-
{expanded && source.sublists && source.sublists.length > 0 && (
|
|
100
|
+
{(headless || expanded) && source.sublists && source.sublists.length > 0 && (
|
|
98
101
|
<tr>
|
|
99
102
|
<td colSpan={colSpan} className={'px-6 pb-4'}>
|
|
100
103
|
<div className={'mt-1 border-t pt-3'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
@@ -128,7 +131,7 @@ const DomainRow = ({ source, searchedAt, developerName }: { source: DomainSource
|
|
|
128
131
|
);
|
|
129
132
|
|
|
130
133
|
|
|
131
|
-
const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedAt, developerName, genreMapping }) => {
|
|
134
|
+
const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedAt, developerName, genreMapping, headless }) => {
|
|
132
135
|
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
|
|
133
136
|
const [expanded, setExpanded] = useState<{ [k: number]: boolean }>({});
|
|
134
137
|
|
|
@@ -227,7 +230,7 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
227
230
|
|
|
228
231
|
const visibleParsedSources = (type === 'business_rules')
|
|
229
232
|
? sortedParsedSources
|
|
230
|
-
: sortedParsedSources.slice(0, visibleCount);
|
|
233
|
+
: (headless ? sortedParsedSources : sortedParsedSources.slice(0, visibleCount));
|
|
231
234
|
|
|
232
235
|
const handleLoadMore = () => {
|
|
233
236
|
setVisibleCount(currentCount => currentCount + PAGE_SIZE);
|
|
@@ -238,8 +241,8 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
return (
|
|
241
|
-
<div>
|
|
242
|
-
<div className={'overflow-x-auto rounded-lg border'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
|
|
244
|
+
<div className={'kyd-avoid-break'}>
|
|
245
|
+
<div className={'overflow-x-auto rounded-lg border kyd-avoid-break'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
243
246
|
<table className={'min-w-full'}>
|
|
244
247
|
<colgroup>
|
|
245
248
|
{headers.map((_, idx) => (
|
|
@@ -268,6 +271,7 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
268
271
|
onToggle={() => setExpanded(prev => ({ ...prev, [index]: !prev[index] }))}
|
|
269
272
|
expanded={!!expanded[index]}
|
|
270
273
|
colSpan={headers.length}
|
|
274
|
+
headless={headless}
|
|
271
275
|
/>
|
|
272
276
|
);
|
|
273
277
|
}
|
|
@@ -299,9 +303,11 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
299
303
|
<td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>{br.category || '—'}</td>
|
|
300
304
|
<td className={'px-4 py-4 whitespace-normal text-sm font-medium'} style={{ color: 'var(--text-main)' }}>{br.label || '—'}</td>
|
|
301
305
|
<td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>{(() => {
|
|
302
|
-
const weight = Number(br.weight
|
|
303
|
-
if (!Number.isFinite(weight)) return '
|
|
304
|
-
|
|
306
|
+
const weight = Number(br.weight);
|
|
307
|
+
if (!Number.isFinite(weight)) return 'Neutral';
|
|
308
|
+
if (weight > 0) return 'Positive';
|
|
309
|
+
if (weight < 0) return 'Negative';
|
|
310
|
+
return 'Neutral';
|
|
305
311
|
})()}</td>
|
|
306
312
|
</tr>
|
|
307
313
|
);
|
|
@@ -309,7 +315,7 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
|
|
|
309
315
|
</tbody>
|
|
310
316
|
</table>
|
|
311
317
|
</div>
|
|
312
|
-
{type !== 'business_rules' && parsedSources.length > PAGE_SIZE && (
|
|
318
|
+
{type !== 'business_rules' && parsedSources.length > PAGE_SIZE && !headless && (
|
|
313
319
|
<div className={'mt-4 flex items-center justify-between text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
314
320
|
<p>
|
|
315
321
|
Showing {Math.min(visibleCount, parsedSources.length)} of {parsedSources.length} entries
|
|
@@ -48,12 +48,34 @@ const GraphInsights = ({
|
|
|
48
48
|
categories,
|
|
49
49
|
genre,
|
|
50
50
|
scoringSummary,
|
|
51
|
+
headless,
|
|
51
52
|
}: {
|
|
52
53
|
graphInsights: GraphInsightsPayload;
|
|
53
54
|
categories?: string[];
|
|
54
55
|
genre: string;
|
|
55
56
|
scoringSummary?: ScoringSummary;
|
|
57
|
+
headless?: boolean;
|
|
56
58
|
}) => {
|
|
59
|
+
const disableAnimations = React.useMemo(() => {
|
|
60
|
+
// Disable chart animations for headless/PDF/print to avoid capturing initial frames
|
|
61
|
+
if (headless) return true;
|
|
62
|
+
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
|
63
|
+
try {
|
|
64
|
+
return window.matchMedia('print').matches;
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}, [headless]);
|
|
69
|
+
|
|
70
|
+
// Nudge ResponsiveContainer to measure after mount in headless contexts
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (typeof window !== 'undefined') {
|
|
73
|
+
const id = window.setTimeout(() => {
|
|
74
|
+
try { window.dispatchEvent(new Event('resize')); } catch {}
|
|
75
|
+
}, 0);
|
|
76
|
+
return () => window.clearTimeout(id);
|
|
77
|
+
}
|
|
78
|
+
}, []);
|
|
57
79
|
const getCategoryTooltipCopy = (category: string): string => {
|
|
58
80
|
const name = (category || '').toLowerCase();
|
|
59
81
|
|
|
@@ -231,12 +253,12 @@ const GraphInsights = ({
|
|
|
231
253
|
<div className="grid grid-cols-1 gap-6">
|
|
232
254
|
{/* Spider Chart: Category Balance (genre-scoped) */}
|
|
233
255
|
{radarData && radarData.length > 2 && (
|
|
234
|
-
<div className={'rounded-lg p-4 pb-4 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
235
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6" >
|
|
256
|
+
<div className={'rounded-lg p-4 pb-4 border kyd-avoid-break'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
257
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 kyd-avoid-break" >
|
|
236
258
|
|
|
237
259
|
{/* Spider Chart: Category Scores */}
|
|
238
|
-
<div className="" style={{ width: '100%', height: 450 }}>
|
|
239
|
-
<div className="relative" style={{ width: '100%', height: 375 }}>
|
|
260
|
+
<div className="kyd-avoid-break" style={{ width: '100%', height: 450, breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
261
|
+
<div className="relative kyd-avoid-break" style={{ width: '100%', height: 375, breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
240
262
|
<div className="mb-2">
|
|
241
263
|
<div className={'font-medium'} style={{ color: 'var(--text-main)' }}>{genre ? `${genre} ` : ''} Category Contributions - Percentages</div>
|
|
242
264
|
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>The spider diagram displays the KYD {genre} score across its sub-categories, with each point representing the strength of available evidence signals</div>
|
|
@@ -247,13 +269,13 @@ const GraphInsights = ({
|
|
|
247
269
|
<PolarAngleAxis dataKey="axis" tick={renderAngleTick} />
|
|
248
270
|
<PolarRadiusAxis angle={55} domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} className="text-sm" />
|
|
249
271
|
{/* Primary area */}
|
|
250
|
-
<Radar name="Category Score" dataKey="score" stroke={'var(--text-main)'} fill={'var(--text-main)'} fillOpacity={0.22} />
|
|
272
|
+
<Radar name="Category Score" dataKey="score" stroke={'var(--text-main)'} fill={'var(--text-main)'} fillOpacity={0.22} isAnimationActive={!disableAnimations} />
|
|
251
273
|
{/* Subtle average ring (arbitrary benchmark for comparison) */}
|
|
252
|
-
<Radar name="Avg" dataKey="avg" stroke="rgba(128,128,128,0.6)" fill="rgba(128,128,128,0.08)" strokeDasharray="4 4" fillOpacity={1} />
|
|
274
|
+
<Radar name="Avg" dataKey="avg" stroke="rgba(128,128,128,0.6)" fill="rgba(128,128,128,0.08)" strokeDasharray="4 4" fillOpacity={1} isAnimationActive={!disableAnimations} />
|
|
253
275
|
<Tooltip content={<RadarCategoryTooltip />} />
|
|
254
276
|
</RadarChart>
|
|
255
277
|
</ResponsiveContainer>
|
|
256
|
-
{labelHover && (
|
|
278
|
+
{!headless && labelHover && (
|
|
257
279
|
<div
|
|
258
280
|
className="pointer-events-none absolute z-30"
|
|
259
281
|
style={{
|
|
@@ -285,8 +307,8 @@ const GraphInsights = ({
|
|
|
285
307
|
</div>
|
|
286
308
|
|
|
287
309
|
{/* Pie Chart: Category Distribution */}
|
|
288
|
-
<div className="md:border-l md:pl-6" style={{ borderColor: 'var(--icon-button-secondary)', width: '100%', height: 450 }}>
|
|
289
|
-
<div className="" style={{ borderColor: 'var(--icon-button-secondary)', width: '100%', height: 375 }}>
|
|
310
|
+
<div className="md:border-l md:pl-6 kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)', width: '100%', height: 450, breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
311
|
+
<div className="kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)', width: '100%', height: 375, breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
290
312
|
<div className="mb-2">
|
|
291
313
|
<div className={'font-medium'} style={{ color: 'var(--text-main)' }}>{genre ? `${genre} ` : ''} Category Contributions - Proportions</div>
|
|
292
314
|
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>The donut diagram illustrates the relative contribution of each {genre} category to the pillar’s composite score.</div>
|
|
@@ -325,7 +347,7 @@ const GraphInsights = ({
|
|
|
325
347
|
startAngle={90}
|
|
326
348
|
endAngle={-270}
|
|
327
349
|
paddingAngle={1.5}
|
|
328
|
-
isAnimationActive
|
|
350
|
+
isAnimationActive={!disableAnimations}
|
|
329
351
|
label={renderLabel}
|
|
330
352
|
labelLine={false}
|
|
331
353
|
>
|
|
@@ -166,7 +166,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
|
166
166
|
);
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCategoryRadar?: SkillsRadarPoint[] }) => {
|
|
169
|
+
const Skills = ({ skillsCategoryRadar, headless }: { skillsMatrix?: SkillsMatrix; skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) => {
|
|
170
170
|
// const hasMatrix = !!(skillsMatrix && Array.isArray(skillsMatrix.skills) && skillsMatrix.skills.length > 0);
|
|
171
171
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
172
172
|
|
|
@@ -180,6 +180,16 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
180
180
|
const [footprintLegendTooltip, setFootprintLegendTooltip] = useState<HoverTooltipState>(null);
|
|
181
181
|
const [barLegendTooltip, setBarLegendTooltip] = useState<HoverTooltipState>(null);
|
|
182
182
|
|
|
183
|
+
const disableAnimations = useMemo(() => {
|
|
184
|
+
if (headless) return true;
|
|
185
|
+
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
|
186
|
+
try {
|
|
187
|
+
return window.matchMedia('print').matches;
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}, [headless]);
|
|
192
|
+
|
|
183
193
|
useEffect(() => {
|
|
184
194
|
const measure = () => setContainerWidth(containerRef.current?.clientWidth || 0);
|
|
185
195
|
measure();
|
|
@@ -187,6 +197,16 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
187
197
|
return () => window.removeEventListener('resize', measure);
|
|
188
198
|
}, []);
|
|
189
199
|
|
|
200
|
+
// Nudge layout for headless/print contexts so responsive containers measure correctly
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (typeof window !== 'undefined') {
|
|
203
|
+
const id = window.setTimeout(() => {
|
|
204
|
+
try { window.dispatchEvent(new Event('resize')); } catch {}
|
|
205
|
+
}, 0);
|
|
206
|
+
return () => window.clearTimeout(id);
|
|
207
|
+
}
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
190
210
|
const combinedBubbleData: BubbleDatum[] = useMemo(() => {
|
|
191
211
|
return skillsRadarLimited.map((d) => {
|
|
192
212
|
const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
|
|
@@ -210,11 +230,11 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
210
230
|
|
|
211
231
|
{/* Skills Coverage and Breakdown: two charts side-by-side */}
|
|
212
232
|
{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)' }}>
|
|
233
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 kyd-avoid-break">
|
|
234
|
+
<div className={'rounded-lg p-4 border kyd-avoid-break'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
215
235
|
<h4 className={'font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
|
|
216
236
|
<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' }}>
|
|
237
|
+
<div ref={containerRef} className={'kyd-avoid-break'} style={{ width: '100%', height: 340, position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
218
238
|
{(() => {
|
|
219
239
|
const width = containerWidth || 600;
|
|
220
240
|
const height = 300;
|
|
@@ -276,12 +296,12 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
276
296
|
</svg>
|
|
277
297
|
);
|
|
278
298
|
})()}
|
|
279
|
-
<TooltipBox state={footprintChartTooltip} />
|
|
299
|
+
{!headless && <TooltipBox state={footprintChartTooltip} />}
|
|
280
300
|
</div>
|
|
281
301
|
{/* Legend */}
|
|
282
302
|
<div className={'mt-3'}>
|
|
283
303
|
{/* <div className={'text-xs mb-2'} style={{ color: 'var(--text-secondary)' }}>Legend</div> */}
|
|
284
|
-
<div ref={footprintLegendRef} style={{ position: 'relative' }}>
|
|
304
|
+
<div ref={footprintLegendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
285
305
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
|
|
286
306
|
{legendData.map((item, idx) => (
|
|
287
307
|
<div
|
|
@@ -314,11 +334,11 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
314
334
|
</div>
|
|
315
335
|
))}
|
|
316
336
|
</div>
|
|
317
|
-
<TooltipBox state={footprintLegendTooltip} />
|
|
337
|
+
{!headless && <TooltipBox state={footprintLegendTooltip} />}
|
|
318
338
|
</div>
|
|
319
339
|
</div>
|
|
320
340
|
</div>
|
|
321
|
-
<div className={'rounded-lg p-4 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
341
|
+
<div className={'rounded-lg p-4 border kyd-avoid-break'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
322
342
|
<h4 className={'font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills by Validation Type</h4>
|
|
323
343
|
<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
344
|
<div className="space-y-6 flex-col items-center" >
|
|
@@ -330,9 +350,9 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
330
350
|
<XAxis dataKey="axis" tick={{ fill: 'var(--text-secondary)', fontSize: 12 }} interval={0} angle={-20} textAnchor="end" height={50} />
|
|
331
351
|
<YAxis domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} />
|
|
332
352
|
<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)'} />
|
|
353
|
+
<Bar dataKey="observed" name="Observed" fill={'var(--bar-observed)'} isAnimationActive={!disableAnimations} />
|
|
354
|
+
<Bar dataKey="self_reported" name="Self-reported" fill={'var(--bar-self-reported)'} isAnimationActive={!disableAnimations} />
|
|
355
|
+
<Bar dataKey="certified" name="Certified" fill={'var(--bar-certified)'} isAnimationActive={!disableAnimations} />
|
|
336
356
|
</BarChart>
|
|
337
357
|
</ResponsiveContainer>
|
|
338
358
|
</div>
|
|
@@ -409,7 +429,7 @@ const Skills = ({ skillsCategoryRadar }: { skillsMatrix?: SkillsMatrix; skillsCa
|
|
|
409
429
|
<span>Certified</span>
|
|
410
430
|
</div>
|
|
411
431
|
</div>
|
|
412
|
-
<TooltipBox state={barLegendTooltip} />
|
|
432
|
+
{!headless && <TooltipBox state={barLegendTooltip} />}
|
|
413
433
|
</div>
|
|
414
434
|
</div>
|
|
415
435
|
</div>
|