kyd-shared-badge 0.2.28 → 0.2.30
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
|
@@ -20,6 +20,7 @@ import Skills from './components/Skills';
|
|
|
20
20
|
import CategoryBars from './components/CategoryBars';
|
|
21
21
|
import SkillsAppendixTable from './components/SkillsAppendixTable';
|
|
22
22
|
import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
23
|
+
import Reveal from './components/Reveal';
|
|
23
24
|
|
|
24
25
|
// const hexToRgba = (hex: string, alpha: number) => {
|
|
25
26
|
// const clean = hex.replace('#', '');
|
|
@@ -138,16 +139,18 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
138
139
|
<BusinessRulesProvider items={graphInsights?.business_rules_all}>
|
|
139
140
|
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
140
141
|
{/* Share controls removed; app-level pages render their own actions */}
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
<Reveal offsetY={8} durationMs={500}>
|
|
143
|
+
<ReportHeader
|
|
144
|
+
badgeId={badgeId}
|
|
145
|
+
developerName={badgeData.developerName}
|
|
146
|
+
updatedAt={updatedAt}
|
|
147
|
+
score={overallFinalPercent || 0}
|
|
148
|
+
isPublic={true}
|
|
149
|
+
badgeImageUrl={badgeData.badgeImageUrl || ''}
|
|
150
|
+
summary={report_summary}
|
|
151
|
+
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
152
|
+
/>
|
|
153
|
+
</Reveal>
|
|
151
154
|
<div
|
|
152
155
|
className={'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
|
|
153
156
|
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
|
|
@@ -155,7 +158,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
155
158
|
|
|
156
159
|
<div className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
157
160
|
<div className="pt-8 first:pt-0">
|
|
158
|
-
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</
|
|
161
|
+
<Reveal as={'h4'} offsetY={8} durationMs={500} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</Reveal>
|
|
159
162
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
160
163
|
{/* Technical semicircle gauge (refactored) */}
|
|
161
164
|
{(() => {
|
|
@@ -164,15 +167,17 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
164
167
|
const label = ui?.label || 'EVIDENCE';
|
|
165
168
|
const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Technical');
|
|
166
169
|
return (
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
<Reveal delayMs={0} offsetY={12}>
|
|
171
|
+
<GaugeCard
|
|
172
|
+
key={'technical-card'}
|
|
173
|
+
title={'KYD Technical'}
|
|
174
|
+
description={'The gauge visualization shows a weighted composite of technical evidence, with rightward movement indicating stronger indications of developer capability'}
|
|
175
|
+
percent={pct}
|
|
176
|
+
label={label}
|
|
177
|
+
topMovers={top?.map(t => ({ label: t?.label, uid: t?.uid }))}
|
|
178
|
+
topMoversTitle={'Top Score Movers'}
|
|
179
|
+
/>
|
|
180
|
+
</Reveal>
|
|
176
181
|
);
|
|
177
182
|
})()}
|
|
178
183
|
|
|
@@ -184,15 +189,17 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
184
189
|
const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Risk');
|
|
185
190
|
const tooltip = 'Higher bar filled indicates lower overall risk; movement to the right reflects improved risk posture.';
|
|
186
191
|
return (
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
<Reveal delayMs={80} offsetY={12}>
|
|
193
|
+
<RiskCard
|
|
194
|
+
title={'KYD Risk'}
|
|
195
|
+
description={'The bar chart visualizes relative risk levels, where shorter bars denote lower risk and taller bars indicate greater exposure.'}
|
|
196
|
+
percentGood={pctGood}
|
|
197
|
+
label={label}
|
|
198
|
+
topMovers={top?.map(t => ({ label: t?.label, uid: t?.uid }))}
|
|
199
|
+
topMoversTitle={'Top Score Movers'}
|
|
200
|
+
tooltipText={tooltip}
|
|
201
|
+
/>
|
|
202
|
+
</Reveal>
|
|
196
203
|
);
|
|
197
204
|
})()}
|
|
198
205
|
|
|
@@ -202,16 +209,18 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
202
209
|
const label = 'AI Transparency'// TODO: calculate label frontend
|
|
203
210
|
const topMovers = ai_usage_summary?.key_findings || []
|
|
204
211
|
return (
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
<Reveal delayMs={160} offsetY={12}>
|
|
213
|
+
<GaugeCard
|
|
214
|
+
key={'ai-card'}
|
|
215
|
+
title={'KYD AI'}
|
|
216
|
+
description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
|
|
217
|
+
percent={ai_usage_summary?.transparency_score}
|
|
218
|
+
label={label}
|
|
219
|
+
// id non-functional
|
|
220
|
+
topMovers={topMovers.map(t => ({ label: t, uid: 'ai-usage' }))}
|
|
221
|
+
topMoversTitle={'Key Findings'}
|
|
222
|
+
/>
|
|
223
|
+
</Reveal>
|
|
215
224
|
);
|
|
216
225
|
})()}
|
|
217
226
|
</div>
|
|
@@ -220,22 +229,24 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
220
229
|
{/* Technical Scores */}
|
|
221
230
|
<div className="mt-8" >
|
|
222
231
|
<div key={'Technical'} className='pt-8 space-y-8' style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
223
|
-
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical</
|
|
232
|
+
<Reveal as={'h4'} offsetY={8} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical</Reveal>
|
|
224
233
|
{/* technical graph insights */}
|
|
225
|
-
<
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
<Reveal>
|
|
235
|
+
<div className="">
|
|
236
|
+
<GraphInsights
|
|
237
|
+
graphInsights={graphInsights}
|
|
238
|
+
categories={genreMapping?.['Technical'] as string[]}
|
|
239
|
+
genre={'Technical'}
|
|
240
|
+
scoringSummary={scoringSummary}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
</Reveal>
|
|
233
244
|
|
|
234
245
|
{/* category bars and contributing factors */}
|
|
235
246
|
<div className="grid grid-cols-1 lg:grid-cols-12 w-full gap-8 items-stretch py-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
236
247
|
|
|
237
248
|
{/* Left: Bars */}
|
|
238
|
-
<
|
|
249
|
+
<Reveal className="lg:col-span-8 h-full">
|
|
239
250
|
<CategoryBars
|
|
240
251
|
title={'Technical Category Contributions - Percentages'}
|
|
241
252
|
categories={genreMapping?.['Technical'] as string[]}
|
|
@@ -244,10 +255,10 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
244
255
|
getCategoryTooltipCopy={getCategoryTooltipCopy}
|
|
245
256
|
barHeight={16}
|
|
246
257
|
/>
|
|
247
|
-
</
|
|
258
|
+
</Reveal>
|
|
248
259
|
|
|
249
260
|
{/* Right: Contributing Factors */}
|
|
250
|
-
<
|
|
261
|
+
<Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
|
|
251
262
|
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
252
263
|
<div className="space-y-4">
|
|
253
264
|
{(genreMapping?.['Technical'] || []).map((cat: string) => {
|
|
@@ -276,15 +287,17 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
276
287
|
);
|
|
277
288
|
})}
|
|
278
289
|
</div>
|
|
279
|
-
</
|
|
290
|
+
</Reveal>
|
|
280
291
|
</div>
|
|
281
292
|
|
|
282
|
-
<
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
<
|
|
293
|
+
<Reveal>
|
|
294
|
+
<div className="pt-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
295
|
+
<h3 className={'text-xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
296
|
+
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
297
|
+
<Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} />
|
|
298
|
+
</div>
|
|
286
299
|
</div>
|
|
287
|
-
</
|
|
300
|
+
</Reveal>
|
|
288
301
|
|
|
289
302
|
</div>
|
|
290
303
|
</div>
|
|
@@ -292,20 +305,22 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
292
305
|
|
|
293
306
|
|
|
294
307
|
<div className="pt-8 space-y-8">
|
|
295
|
-
<h3 className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</
|
|
308
|
+
<Reveal as={'h3'} offsetY={8} className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</Reveal>
|
|
296
309
|
|
|
297
310
|
{/* Risk Graph Insights and Category Bars */}
|
|
298
|
-
<
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
<Reveal>
|
|
312
|
+
<div className="">
|
|
313
|
+
<GraphInsights
|
|
314
|
+
graphInsights={graphInsights}
|
|
315
|
+
categories={genreMapping?.['Risk'] as string[]}
|
|
316
|
+
genre={'Risk'}
|
|
317
|
+
scoringSummary={scoringSummary}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
</Reveal>
|
|
306
321
|
<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)' }}>
|
|
307
322
|
{/* Left: Bars */}
|
|
308
|
-
<
|
|
323
|
+
<Reveal className="lg:col-span-8 h-full">
|
|
309
324
|
<CategoryBars
|
|
310
325
|
title={'KYD Risk - Category Insights'}
|
|
311
326
|
categories={genreMapping?.['Risk'] as string[]}
|
|
@@ -314,9 +329,9 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
314
329
|
getCategoryTooltipCopy={getCategoryTooltipCopy}
|
|
315
330
|
barHeight={16}
|
|
316
331
|
/>
|
|
317
|
-
</
|
|
332
|
+
</Reveal>
|
|
318
333
|
{/* Right: Contributing Factors */}
|
|
319
|
-
<
|
|
334
|
+
<Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
|
|
320
335
|
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
321
336
|
<div className="space-y-4">
|
|
322
337
|
{genreMapping?.['Risk']?.map((cat: string) => {
|
|
@@ -345,12 +360,12 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
345
360
|
);
|
|
346
361
|
})}
|
|
347
362
|
</div>
|
|
348
|
-
</
|
|
363
|
+
</Reveal>
|
|
349
364
|
</div>
|
|
350
365
|
|
|
351
366
|
{/* cyber risk display */}
|
|
352
367
|
{badgeData.optOutScreening ? (
|
|
353
|
-
<
|
|
368
|
+
<Reveal className={'p-4 rounded-lg border'} style={{ backgroundColor: 'var(--icon-button-secondary)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
354
369
|
<div className="flex items-start">
|
|
355
370
|
<span className="h-5 w-5 mr-3 mt-0.5 flex-shrink-0" style={{ color: yellow }}>
|
|
356
371
|
<FiAlertTriangle size={20} />
|
|
@@ -362,7 +377,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
362
377
|
</p>
|
|
363
378
|
</div>
|
|
364
379
|
</div>
|
|
365
|
-
</
|
|
380
|
+
</Reveal>
|
|
366
381
|
) : (
|
|
367
382
|
<>
|
|
368
383
|
{(() => {
|
|
@@ -372,7 +387,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
372
387
|
const fbiMatches = ss?.fbi_matches && (ss.fbi_matches.length > 0);
|
|
373
388
|
if (!(ofacMatches || cslDetails || fbiMatches)) return null;
|
|
374
389
|
return (
|
|
375
|
-
<
|
|
390
|
+
<Reveal className={'mb-8 rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
|
|
376
391
|
<h4 className={'text-lg font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>3A. Sanctions Matches</h4>
|
|
377
392
|
{/* OFAC matches */}
|
|
378
393
|
{ofacMatches && (
|
|
@@ -432,17 +447,21 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
432
447
|
</div>
|
|
433
448
|
</div>
|
|
434
449
|
)}
|
|
435
|
-
</
|
|
450
|
+
</Reveal>
|
|
436
451
|
);
|
|
437
452
|
})()}
|
|
438
|
-
<
|
|
453
|
+
<Reveal>
|
|
454
|
+
<IpRiskAnalysisDisplay ipRiskAnalysis={screening_sources?.ip_risk_analysis} />
|
|
455
|
+
</Reveal>
|
|
439
456
|
</>
|
|
440
457
|
)}
|
|
441
458
|
|
|
442
459
|
</div>
|
|
443
460
|
|
|
444
461
|
{/* Connected Platforms */}
|
|
445
|
-
<
|
|
462
|
+
<Reveal>
|
|
463
|
+
<ConnectedPlatforms accounts={connected} />
|
|
464
|
+
</Reveal>
|
|
446
465
|
|
|
447
466
|
|
|
448
467
|
<div className="pt-8">
|
|
@@ -450,30 +469,35 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
450
469
|
<div className="space-y-8">
|
|
451
470
|
|
|
452
471
|
{/* Skills */}
|
|
453
|
-
<
|
|
454
|
-
<
|
|
455
|
-
|
|
456
|
-
|
|
472
|
+
<Reveal>
|
|
473
|
+
<div>
|
|
474
|
+
<h4 id="appendix-skills" className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Skills</h4>
|
|
475
|
+
<SkillsAppendixTable skillsAll={skillsAll} />
|
|
476
|
+
</div>
|
|
477
|
+
</Reveal>
|
|
457
478
|
|
|
458
479
|
{/* Observations */}
|
|
459
480
|
{Array.isArray(graphInsights?.business_rules_all) && graphInsights.business_rules_all.length > 0 && (
|
|
460
|
-
<
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
481
|
+
<Reveal>
|
|
482
|
+
<div>
|
|
483
|
+
<h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Observations</h4>
|
|
484
|
+
<AppendixTables
|
|
485
|
+
type="business_rules"
|
|
486
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
487
|
+
sources={graphInsights.business_rules_all as any}
|
|
488
|
+
searchedAt={updatedAt}
|
|
489
|
+
developerName={developerName || 'this developer'}
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
</Reveal>
|
|
470
493
|
)}
|
|
471
494
|
|
|
472
495
|
{/* Sanctions & Watchlists */}
|
|
473
496
|
{!badgeData.optOutScreening && screening_sources && (
|
|
474
|
-
<
|
|
475
|
-
<
|
|
476
|
-
|
|
497
|
+
<Reveal>
|
|
498
|
+
<div>
|
|
499
|
+
<h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Sanctions & Watchlists</h4>
|
|
500
|
+
{(() => {
|
|
477
501
|
const ofacProvided = screening_sources.ofac_screen?.sources || [];
|
|
478
502
|
const lists = [...(screening_sources.ofac_lists || []), ...ofacProvided];
|
|
479
503
|
const seen: { [k: string]: boolean } = {};
|
|
@@ -493,29 +517,34 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
493
517
|
/>
|
|
494
518
|
);
|
|
495
519
|
})()}
|
|
496
|
-
|
|
520
|
+
</div>
|
|
521
|
+
</Reveal>
|
|
497
522
|
)}
|
|
498
523
|
|
|
499
524
|
</div>
|
|
500
525
|
</div>
|
|
501
526
|
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
527
|
+
<Reveal>
|
|
528
|
+
<div className={'pt-8 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
529
|
+
Report Completed: {new Date(updatedAt).toLocaleString(undefined, {
|
|
530
|
+
year: 'numeric',
|
|
531
|
+
month: 'long',
|
|
532
|
+
day: 'numeric',
|
|
533
|
+
hour: 'numeric',
|
|
534
|
+
minute: '2-digit',
|
|
535
|
+
timeZoneName: 'short',
|
|
536
|
+
})}
|
|
537
|
+
</div>
|
|
538
|
+
</Reveal>
|
|
512
539
|
</div>
|
|
513
540
|
</div>
|
|
514
|
-
<
|
|
515
|
-
<
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
541
|
+
<Reveal>
|
|
542
|
+
<footer className={'mt-12 pt-6 border-t'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
543
|
+
<p className={'text-center text-xs max-w-4xl mx-auto'} style={{ color: 'var(--text-secondary)' }}>
|
|
544
|
+
© 2025 Know Your Developer, LLC. All rights reserved. KYD Self-Check™, and associated marks are trademarks of Know Your Developer, LLC. This document is confidential, proprietary, and intended solely for the individual or entity to whom it is addressed. Unauthorized use, disclosure, copying, or distribution of this document or any of its contents is strictly prohibited and may be unlawful. Know Your Developer, LLC assumes no responsibility or liability for any errors or omissions contained herein. Report validity subject to the terms and conditions stated on the official Know Your Developer website located at https://knowyourdeveloper.ai.
|
|
545
|
+
</p>
|
|
546
|
+
</footer>
|
|
547
|
+
</Reveal>
|
|
519
548
|
</div>
|
|
520
549
|
</BusinessRulesProvider>
|
|
521
550
|
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { CSSProperties, PropsWithChildren } from 'react';
|
|
4
|
+
import { useInViewOnce } from './useInViewOnce';
|
|
5
|
+
|
|
6
|
+
type RevealProps = PropsWithChildren<{
|
|
7
|
+
as?: keyof JSX.IntrinsicElements;
|
|
8
|
+
/** px to translate on Y axis initially */
|
|
9
|
+
offsetY?: number;
|
|
10
|
+
/** px to translate on X axis initially (rarely needed) */
|
|
11
|
+
offsetX?: number;
|
|
12
|
+
/** ms */
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
/** ms */
|
|
15
|
+
delayMs?: number;
|
|
16
|
+
/** Optional className passed to the wrapper */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Optional style merged into wrapper */
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
/** Intersection thresholds */
|
|
21
|
+
threshold?: number | number[];
|
|
22
|
+
/** Root margin for preloading */
|
|
23
|
+
rootMargin?: string;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
const Reveal: React.FC<RevealProps> = ({
|
|
27
|
+
as = 'div',
|
|
28
|
+
offsetY = 12,
|
|
29
|
+
offsetX = 0,
|
|
30
|
+
durationMs = 500,
|
|
31
|
+
delayMs = 0,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
threshold,
|
|
35
|
+
rootMargin,
|
|
36
|
+
children,
|
|
37
|
+
}) => {
|
|
38
|
+
const Tag = as as any;
|
|
39
|
+
const { ref, hasIntersected } = useInViewOnce({ threshold, rootMargin });
|
|
40
|
+
|
|
41
|
+
// Respect reduced motion is handled in the hook by triggering immediately.
|
|
42
|
+
const baseTransform = `translate(${offsetX}px, ${offsetY}px)`;
|
|
43
|
+
const eased = 'cubic-bezier(0.22, 1, 0.36, 1)'; // subtle spring-ish ease
|
|
44
|
+
|
|
45
|
+
const inlineStyles: CSSProperties = {
|
|
46
|
+
opacity: hasIntersected ? 1 : 0,
|
|
47
|
+
transform: hasIntersected ? 'none' : baseTransform,
|
|
48
|
+
transition: `opacity ${durationMs}ms ${eased} ${delayMs}ms, transform ${durationMs}ms ${eased} ${delayMs}ms`,
|
|
49
|
+
willChange: hasIntersected ? undefined : 'opacity, transform',
|
|
50
|
+
...style,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Tag ref={ref} className={className} style={inlineStyles}>
|
|
55
|
+
{children}
|
|
56
|
+
</Tag>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default Reveal;
|
|
61
|
+
|
|
62
|
+
|
|
@@ -26,9 +26,9 @@ export default function RiskCard({
|
|
|
26
26
|
const pctGood = Math.max(0, Math.min(100, Math.round(Number(percentGood ?? 0))));
|
|
27
27
|
const displayLabel = label || '';
|
|
28
28
|
|
|
29
|
-
// bar heights
|
|
30
|
-
const bars = [
|
|
31
|
-
let activeIndex = 0; // Default to the
|
|
29
|
+
// bar heights ascending representation
|
|
30
|
+
const bars = [40, 60, 85, 110, 140];
|
|
31
|
+
let activeIndex = 0; // Default to the shortest bar (highest risk)
|
|
32
32
|
if (pctGood >= 80) {
|
|
33
33
|
activeIndex = 4;
|
|
34
34
|
} else if (pctGood >= 60) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Observes the provided element and returns true the first time it enters the viewport.
|
|
7
|
+
* Uses IntersectionObserver under the hood and disconnects after first intersection.
|
|
8
|
+
*/
|
|
9
|
+
export function useInViewOnce<T extends HTMLElement>(options?: IntersectionObserverInit) {
|
|
10
|
+
const targetRef = useRef<T | null>(null);
|
|
11
|
+
const [hasIntersected, setHasIntersected] = useState<boolean>(false);
|
|
12
|
+
const [hasMounted, setHasMounted] = useState<boolean>(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setHasMounted(true);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (hasIntersected) return; // already observed once
|
|
20
|
+
const element = targetRef.current;
|
|
21
|
+
if (!element) return;
|
|
22
|
+
|
|
23
|
+
// Respect reduced motion
|
|
24
|
+
const prefersReducedMotion = typeof window !== 'undefined' &&
|
|
25
|
+
window.matchMedia &&
|
|
26
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
+
if (prefersReducedMotion) {
|
|
28
|
+
setHasIntersected(true);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const observer = new IntersectionObserver(
|
|
33
|
+
(entries, obs) => {
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.isIntersecting) {
|
|
36
|
+
setHasIntersected(true);
|
|
37
|
+
obs.disconnect();
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
root: options?.root || null,
|
|
44
|
+
rootMargin: options?.rootMargin ?? '0px 0px -10% 0px',
|
|
45
|
+
threshold: options?.threshold ?? 0.1,
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
observer.observe(element);
|
|
50
|
+
return () => {
|
|
51
|
+
observer.disconnect();
|
|
52
|
+
};
|
|
53
|
+
}, [options?.root, options?.rootMargin, options?.threshold, hasIntersected]);
|
|
54
|
+
|
|
55
|
+
return { ref: targetRef, isVisible: hasIntersected || !hasMounted ? false : hasIntersected, hasIntersected } as const;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|