kyd-shared-badge 0.2.29 → 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,7 +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
|
|
23
|
+
import Reveal from './components/Reveal';
|
|
24
24
|
|
|
25
25
|
// const hexToRgba = (hex: string, alpha: number) => {
|
|
26
26
|
// const clean = hex.replace('#', '');
|
|
@@ -139,7 +139,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
139
139
|
<BusinessRulesProvider items={graphInsights?.business_rules_all}>
|
|
140
140
|
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
141
141
|
{/* Share controls removed; app-level pages render their own actions */}
|
|
142
|
-
<
|
|
142
|
+
<Reveal offsetY={8} durationMs={500}>
|
|
143
143
|
<ReportHeader
|
|
144
144
|
badgeId={badgeId}
|
|
145
145
|
developerName={badgeData.developerName}
|
|
@@ -150,16 +150,15 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
150
150
|
summary={report_summary}
|
|
151
151
|
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
152
152
|
/>
|
|
153
|
-
</
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
>
|
|
153
|
+
</Reveal>
|
|
154
|
+
<div
|
|
155
|
+
className={'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
|
|
156
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
|
|
157
|
+
>
|
|
159
158
|
|
|
160
159
|
<div className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
161
160
|
<div className="pt-8 first:pt-0">
|
|
162
|
-
<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>
|
|
163
162
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
164
163
|
{/* Technical semicircle gauge (refactored) */}
|
|
165
164
|
{(() => {
|
|
@@ -168,7 +167,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
168
167
|
const label = ui?.label || 'EVIDENCE';
|
|
169
168
|
const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Technical');
|
|
170
169
|
return (
|
|
171
|
-
<
|
|
170
|
+
<Reveal delayMs={0} offsetY={12}>
|
|
172
171
|
<GaugeCard
|
|
173
172
|
key={'technical-card'}
|
|
174
173
|
title={'KYD Technical'}
|
|
@@ -178,7 +177,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
178
177
|
topMovers={top?.map(t => ({ label: t?.label, uid: t?.uid }))}
|
|
179
178
|
topMoversTitle={'Top Score Movers'}
|
|
180
179
|
/>
|
|
181
|
-
</
|
|
180
|
+
</Reveal>
|
|
182
181
|
);
|
|
183
182
|
})()}
|
|
184
183
|
|
|
@@ -190,7 +189,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
190
189
|
const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Risk');
|
|
191
190
|
const tooltip = 'Higher bar filled indicates lower overall risk; movement to the right reflects improved risk posture.';
|
|
192
191
|
return (
|
|
193
|
-
<
|
|
192
|
+
<Reveal delayMs={80} offsetY={12}>
|
|
194
193
|
<RiskCard
|
|
195
194
|
title={'KYD Risk'}
|
|
196
195
|
description={'The bar chart visualizes relative risk levels, where shorter bars denote lower risk and taller bars indicate greater exposure.'}
|
|
@@ -200,7 +199,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
200
199
|
topMoversTitle={'Top Score Movers'}
|
|
201
200
|
tooltipText={tooltip}
|
|
202
201
|
/>
|
|
203
|
-
</
|
|
202
|
+
</Reveal>
|
|
204
203
|
);
|
|
205
204
|
})()}
|
|
206
205
|
|
|
@@ -210,7 +209,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
210
209
|
const label = 'AI Transparency'// TODO: calculate label frontend
|
|
211
210
|
const topMovers = ai_usage_summary?.key_findings || []
|
|
212
211
|
return (
|
|
213
|
-
<
|
|
212
|
+
<Reveal delayMs={160} offsetY={12}>
|
|
214
213
|
<GaugeCard
|
|
215
214
|
key={'ai-card'}
|
|
216
215
|
title={'KYD AI'}
|
|
@@ -221,7 +220,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
221
220
|
topMovers={topMovers.map(t => ({ label: t, uid: 'ai-usage' }))}
|
|
222
221
|
topMoversTitle={'Key Findings'}
|
|
223
222
|
/>
|
|
224
|
-
</
|
|
223
|
+
</Reveal>
|
|
225
224
|
);
|
|
226
225
|
})()}
|
|
227
226
|
</div>
|
|
@@ -230,9 +229,9 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
230
229
|
{/* Technical Scores */}
|
|
231
230
|
<div className="mt-8" >
|
|
232
231
|
<div key={'Technical'} className='pt-8 space-y-8' style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
233
|
-
<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>
|
|
234
233
|
{/* technical graph insights */}
|
|
235
|
-
<
|
|
234
|
+
<Reveal>
|
|
236
235
|
<div className="">
|
|
237
236
|
<GraphInsights
|
|
238
237
|
graphInsights={graphInsights}
|
|
@@ -241,14 +240,13 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
241
240
|
scoringSummary={scoringSummary}
|
|
242
241
|
/>
|
|
243
242
|
</div>
|
|
244
|
-
</
|
|
243
|
+
</Reveal>
|
|
245
244
|
|
|
246
245
|
{/* category bars and contributing factors */}
|
|
247
|
-
<AnimateOnMount delayMs={160} offsetY={10}>
|
|
248
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)'}}>
|
|
249
247
|
|
|
250
248
|
{/* Left: Bars */}
|
|
251
|
-
<
|
|
249
|
+
<Reveal className="lg:col-span-8 h-full">
|
|
252
250
|
<CategoryBars
|
|
253
251
|
title={'Technical Category Contributions - Percentages'}
|
|
254
252
|
categories={genreMapping?.['Technical'] as string[]}
|
|
@@ -257,10 +255,10 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
257
255
|
getCategoryTooltipCopy={getCategoryTooltipCopy}
|
|
258
256
|
barHeight={16}
|
|
259
257
|
/>
|
|
260
|
-
</
|
|
258
|
+
</Reveal>
|
|
261
259
|
|
|
262
260
|
{/* Right: Contributing Factors */}
|
|
263
|
-
<
|
|
261
|
+
<Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
|
|
264
262
|
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
265
263
|
<div className="space-y-4">
|
|
266
264
|
{(genreMapping?.['Technical'] || []).map((cat: string) => {
|
|
@@ -289,30 +287,28 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
289
287
|
);
|
|
290
288
|
})}
|
|
291
289
|
</div>
|
|
292
|
-
</
|
|
290
|
+
</Reveal>
|
|
293
291
|
</div>
|
|
294
|
-
</AnimateOnMount>
|
|
295
292
|
|
|
296
|
-
<
|
|
293
|
+
<Reveal>
|
|
297
294
|
<div className="pt-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
298
295
|
<h3 className={'text-xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
299
296
|
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
300
297
|
<Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} />
|
|
301
298
|
</div>
|
|
302
299
|
</div>
|
|
303
|
-
</
|
|
300
|
+
</Reveal>
|
|
304
301
|
|
|
305
302
|
</div>
|
|
306
303
|
</div>
|
|
307
304
|
|
|
308
305
|
|
|
309
306
|
|
|
310
|
-
<AnimateOnMount delayMs={200} offsetY={10}>
|
|
311
307
|
<div className="pt-8 space-y-8">
|
|
312
|
-
<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>
|
|
313
309
|
|
|
314
310
|
{/* Risk Graph Insights and Category Bars */}
|
|
315
|
-
<
|
|
311
|
+
<Reveal>
|
|
316
312
|
<div className="">
|
|
317
313
|
<GraphInsights
|
|
318
314
|
graphInsights={graphInsights}
|
|
@@ -321,11 +317,10 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
321
317
|
scoringSummary={scoringSummary}
|
|
322
318
|
/>
|
|
323
319
|
</div>
|
|
324
|
-
</
|
|
325
|
-
<AnimateOnMount delayMs={240} offsetY={10}>
|
|
320
|
+
</Reveal>
|
|
326
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)' }}>
|
|
327
322
|
{/* Left: Bars */}
|
|
328
|
-
<
|
|
323
|
+
<Reveal className="lg:col-span-8 h-full">
|
|
329
324
|
<CategoryBars
|
|
330
325
|
title={'KYD Risk - Category Insights'}
|
|
331
326
|
categories={genreMapping?.['Risk'] as string[]}
|
|
@@ -334,9 +329,9 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
334
329
|
getCategoryTooltipCopy={getCategoryTooltipCopy}
|
|
335
330
|
barHeight={16}
|
|
336
331
|
/>
|
|
337
|
-
</
|
|
332
|
+
</Reveal>
|
|
338
333
|
{/* Right: Contributing Factors */}
|
|
339
|
-
<
|
|
334
|
+
<Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
|
|
340
335
|
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
341
336
|
<div className="space-y-4">
|
|
342
337
|
{genreMapping?.['Risk']?.map((cat: string) => {
|
|
@@ -365,13 +360,12 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
365
360
|
);
|
|
366
361
|
})}
|
|
367
362
|
</div>
|
|
368
|
-
</
|
|
363
|
+
</Reveal>
|
|
369
364
|
</div>
|
|
370
|
-
</AnimateOnMount>
|
|
371
365
|
|
|
372
366
|
{/* cyber risk display */}
|
|
373
367
|
{badgeData.optOutScreening ? (
|
|
374
|
-
<
|
|
368
|
+
<Reveal className={'p-4 rounded-lg border'} style={{ backgroundColor: 'var(--icon-button-secondary)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
375
369
|
<div className="flex items-start">
|
|
376
370
|
<span className="h-5 w-5 mr-3 mt-0.5 flex-shrink-0" style={{ color: yellow }}>
|
|
377
371
|
<FiAlertTriangle size={20} />
|
|
@@ -383,7 +377,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
383
377
|
</p>
|
|
384
378
|
</div>
|
|
385
379
|
</div>
|
|
386
|
-
</
|
|
380
|
+
</Reveal>
|
|
387
381
|
) : (
|
|
388
382
|
<>
|
|
389
383
|
{(() => {
|
|
@@ -393,8 +387,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
393
387
|
const fbiMatches = ss?.fbi_matches && (ss.fbi_matches.length > 0);
|
|
394
388
|
if (!(ofacMatches || cslDetails || fbiMatches)) return null;
|
|
395
389
|
return (
|
|
396
|
-
<
|
|
397
|
-
<div className={'mb-8 rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
|
|
390
|
+
<Reveal className={'mb-8 rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
|
|
398
391
|
<h4 className={'text-lg font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>3A. Sanctions Matches</h4>
|
|
399
392
|
{/* OFAC matches */}
|
|
400
393
|
{ofacMatches && (
|
|
@@ -454,41 +447,38 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
454
447
|
</div>
|
|
455
448
|
</div>
|
|
456
449
|
)}
|
|
457
|
-
</
|
|
458
|
-
</AnimateOnMount>
|
|
450
|
+
</Reveal>
|
|
459
451
|
);
|
|
460
452
|
})()}
|
|
461
|
-
<
|
|
453
|
+
<Reveal>
|
|
462
454
|
<IpRiskAnalysisDisplay ipRiskAnalysis={screening_sources?.ip_risk_analysis} />
|
|
463
|
-
</
|
|
455
|
+
</Reveal>
|
|
464
456
|
</>
|
|
465
457
|
)}
|
|
466
458
|
|
|
467
459
|
</div>
|
|
468
|
-
</AnimateOnMount>
|
|
469
460
|
|
|
470
461
|
{/* Connected Platforms */}
|
|
471
|
-
<
|
|
462
|
+
<Reveal>
|
|
472
463
|
<ConnectedPlatforms accounts={connected} />
|
|
473
|
-
</
|
|
464
|
+
</Reveal>
|
|
474
465
|
|
|
475
466
|
|
|
476
|
-
<AnimateOnMount delayMs={280} offsetY={10}>
|
|
477
467
|
<div className="pt-8">
|
|
478
468
|
<h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix: Data Sources</h3>
|
|
479
469
|
<div className="space-y-8">
|
|
480
470
|
|
|
481
471
|
{/* Skills */}
|
|
482
|
-
<
|
|
472
|
+
<Reveal>
|
|
483
473
|
<div>
|
|
484
474
|
<h4 id="appendix-skills" className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Skills</h4>
|
|
485
475
|
<SkillsAppendixTable skillsAll={skillsAll} />
|
|
486
476
|
</div>
|
|
487
|
-
</
|
|
477
|
+
</Reveal>
|
|
488
478
|
|
|
489
479
|
{/* Observations */}
|
|
490
480
|
{Array.isArray(graphInsights?.business_rules_all) && graphInsights.business_rules_all.length > 0 && (
|
|
491
|
-
<
|
|
481
|
+
<Reveal>
|
|
492
482
|
<div>
|
|
493
483
|
<h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Observations</h4>
|
|
494
484
|
<AppendixTables
|
|
@@ -499,12 +489,12 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
499
489
|
developerName={developerName || 'this developer'}
|
|
500
490
|
/>
|
|
501
491
|
</div>
|
|
502
|
-
</
|
|
492
|
+
</Reveal>
|
|
503
493
|
)}
|
|
504
494
|
|
|
505
495
|
{/* Sanctions & Watchlists */}
|
|
506
496
|
{!badgeData.optOutScreening && screening_sources && (
|
|
507
|
-
<
|
|
497
|
+
<Reveal>
|
|
508
498
|
<div>
|
|
509
499
|
<h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Sanctions & Watchlists</h4>
|
|
510
500
|
{(() => {
|
|
@@ -527,15 +517,14 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
527
517
|
/>
|
|
528
518
|
);
|
|
529
519
|
})()}
|
|
530
|
-
|
|
531
|
-
</
|
|
520
|
+
</div>
|
|
521
|
+
</Reveal>
|
|
532
522
|
)}
|
|
533
523
|
|
|
534
524
|
</div>
|
|
535
525
|
</div>
|
|
536
|
-
</AnimateOnMount>
|
|
537
526
|
|
|
538
|
-
<
|
|
527
|
+
<Reveal>
|
|
539
528
|
<div className={'pt-8 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
540
529
|
Report Completed: {new Date(updatedAt).toLocaleString(undefined, {
|
|
541
530
|
year: 'numeric',
|
|
@@ -546,15 +535,16 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
546
535
|
timeZoneName: 'short',
|
|
547
536
|
})}
|
|
548
537
|
</div>
|
|
549
|
-
</
|
|
538
|
+
</Reveal>
|
|
550
539
|
</div>
|
|
551
540
|
</div>
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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>
|
|
558
548
|
</div>
|
|
559
549
|
</BusinessRulesProvider>
|
|
560
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
|
+
|
|
@@ -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
|
+
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
type AnimateOnMountProps = {
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
delayMs?: number;
|
|
8
|
-
durationMs?: number;
|
|
9
|
-
offsetY?: number; // positive moves up during animation
|
|
10
|
-
className?: string;
|
|
11
|
-
style?: React.CSSProperties;
|
|
12
|
-
as?: 'div' | 'section' | 'span' | 'header' | 'footer' | 'main' | 'article' | 'aside' | 'nav';
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const AnimateOnMount: React.FC<AnimateOnMountProps> = ({
|
|
16
|
-
children,
|
|
17
|
-
delayMs = 60,
|
|
18
|
-
durationMs = 420,
|
|
19
|
-
offsetY = 10,
|
|
20
|
-
className,
|
|
21
|
-
style,
|
|
22
|
-
as = 'div',
|
|
23
|
-
}) => {
|
|
24
|
-
const [mounted, setMounted] = React.useState(false);
|
|
25
|
-
const reduceMotion = React.useMemo(() => {
|
|
26
|
-
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
|
|
27
|
-
try {
|
|
28
|
-
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}, []);
|
|
33
|
-
|
|
34
|
-
React.useEffect(() => {
|
|
35
|
-
if (reduceMotion) {
|
|
36
|
-
// Render immediately with no animation
|
|
37
|
-
setMounted(true);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const id = requestAnimationFrame(() => setMounted(true));
|
|
41
|
-
return () => cancelAnimationFrame(id);
|
|
42
|
-
}, [reduceMotion]);
|
|
43
|
-
|
|
44
|
-
const Component = as as any;
|
|
45
|
-
const transition = `opacity ${durationMs}ms ease, transform ${durationMs}ms ease`;
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<Component
|
|
49
|
-
className={className}
|
|
50
|
-
style={{
|
|
51
|
-
opacity: mounted ? 1 : 0,
|
|
52
|
-
transform: mounted || reduceMotion ? 'translateY(0px)' : `translateY(${offsetY}px)`,
|
|
53
|
-
transition: reduceMotion ? undefined : transition,
|
|
54
|
-
transitionDelay: reduceMotion ? undefined : `${delayMs}ms`,
|
|
55
|
-
willChange: reduceMotion ? undefined : 'opacity, transform',
|
|
56
|
-
...style,
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
{children}
|
|
60
|
-
</Component>
|
|
61
|
-
);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export default AnimateOnMount;
|
|
65
|
-
|
|
66
|
-
|