kyd-shared-badge 0.3.21 → 0.3.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -61,6 +61,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
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, headless }: { badgeData: Pub
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
@@ -159,20 +173,20 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
159
173
  const em = assessmentResult?.enterprise_match;
160
174
  if (!em) return null;
161
175
  const role = em.role || {};
162
- return { score: typeof em.score === 'number' ? em.score : undefined, description: em.description, roleName: role.name };
176
+ return { label: em.label, description: em.description, roleName: role.name };
163
177
  })()}
164
178
  countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
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, headless }: { badgeData: Pub
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, headless }: { badgeData: Pub
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,8 +314,8 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
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
320
  <Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} />
306
321
  </div>
@@ -313,7 +328,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
313
328
 
314
329
 
315
330
  <div className="pt-8 space-y-8">
316
- <Reveal as={'h3'} offsetY={8} className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</Reveal>
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, headless }: { badgeData: Pub
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, headless }: { badgeData: Pub
473
489
 
474
490
 
475
491
  <div className="pt-8">
476
- <h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix</h3>
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 */}
@@ -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
  >
@@ -37,7 +37,7 @@ interface ReportHeaderProps {
37
37
  isPublic: boolean;
38
38
  badgeImageUrl: string;
39
39
  summary?: string;
40
- enterpriseMatch?: { score?: number; description?: string; roleName?: string } | null;
40
+ enterpriseMatch?: { label?: string; description?: string; roleName?: string } | null;
41
41
  countries?: string[];
42
42
  }
43
43
 
@@ -45,8 +45,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
45
45
  // Use the dynamic image if available, otherwise fall back to the score-based one.
46
46
  const finalBadgeImageUrl = badgeImageUrl || getBadgeImageUrl(score || 0);
47
47
  const tint = hexToRgba(pickTint(score || 0), 0.06);
48
- const matchScore = typeof enterpriseMatch?.score === 'number' ? enterpriseMatch.score : undefined;
49
- const matchTint = matchScore !== undefined ? hexToRgba(pickTint(matchScore), 0.06) : undefined;
48
+ const matchLabel = enterpriseMatch?.label;
50
49
 
51
50
  const formattedDate = updatedAt ? formatLocalDate(updatedAt, {
52
51
  year: 'numeric',
@@ -100,7 +99,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
100
99
  })()
101
100
  )}
102
101
  </div>
103
- {(enterpriseMatch?.description || enterpriseMatch?.score !== undefined) ? (
102
+ {(enterpriseMatch?.description || matchLabel) ? (
104
103
  <div className={'hidden md:block text-sm space-y-2 pt-4'} style={{ borderTop: '1px solid var(--icon-button-secondary)' }}>
105
104
  <div className="flex items-center justify-between">
106
105
  <div className="flex items-center gap-2">
@@ -110,14 +109,14 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
110
109
  <div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
111
110
  <div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
112
111
  <div style={{ fontWeight: 600 }}>AI-generated</div>
113
- <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>Role match score and description are AI-generated.</div>
112
+ <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>Role match label and description are AI-generated.</div>
114
113
  </div>
115
114
  </div>
116
115
  </span>
117
116
  </div>
118
- {typeof enterpriseMatch?.score === 'number' && (
119
- <span className="px-2 py-1 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--content-card-background)', backgroundImage: matchTint ? `linear-gradient(${matchTint}, ${matchTint})` : undefined, border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>
120
- {Math.round(enterpriseMatch.score)}%
117
+ {matchLabel && (
118
+ <span className="px-2 py-1 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>
119
+ {matchLabel}
121
120
  </span>
122
121
  )}
123
122
  </div>
package/src/types.ts CHANGED
@@ -159,7 +159,7 @@ export interface EnterpriseMatch {
159
159
  name: string;
160
160
  description: string;
161
161
  };
162
- score: number;
162
+ label: string;
163
163
  description: string;
164
164
  }
165
165