kyd-shared-badge 0.3.86 → 0.3.88
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 +2 -1
- package/src/PrintableBadgeDisplay.tsx +3 -2
- package/src/SharedBadgeDisplay.tsx +90 -22
- package/src/components/ReportHeader.tsx +17 -29
- package/src/components/Skills.tsx +25 -60
- package/src/components/SkillsBubble.tsx +181 -0
- package/src/components/SkillsValidation.tsx +37 -0
- package/src/components/SummaryCards.tsx +3 -13
- package/src/connect/ConnectAccounts.tsx +255 -215
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyd-shared-badge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.88",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@aws-sdk/lib-dynamodb": "^3.893.0",
|
|
22
22
|
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
|
23
23
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
|
24
|
+
"@knowyourdeveloper/react-bubble-chart": "^1.0.0",
|
|
24
25
|
"@radix-ui/react-slot": "^1.2.3",
|
|
25
26
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
26
27
|
"ai": "5.0.47",
|
|
@@ -17,7 +17,8 @@ import GaugeCard from './components/GaugeCard';
|
|
|
17
17
|
import RiskCard from './components/RiskCard';
|
|
18
18
|
|
|
19
19
|
import { yellow, green } from './colors';
|
|
20
|
-
import
|
|
20
|
+
import SkillsValidation from './components/SkillsValidation';
|
|
21
|
+
import SkillsBubble from './components/SkillsBubble';
|
|
21
22
|
import CategoryBars from './components/CategoryBars';
|
|
22
23
|
import SkillsAppendixTable from './components/SkillsAppendixTable';
|
|
23
24
|
import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
@@ -212,7 +213,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
212
213
|
<div className="pt-8 border-t kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
213
214
|
<h3 className={'text-xl font-bold mb-3 kyd-keep-with-next'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
214
215
|
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
215
|
-
<
|
|
216
|
+
<SkillsValidation skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
216
217
|
</div>
|
|
217
218
|
</div>
|
|
218
219
|
</Reveal>
|
|
@@ -15,7 +15,8 @@ import CodeIcon from './components/icons/code';
|
|
|
15
15
|
import RiskIcon from './components/icons/risk';
|
|
16
16
|
|
|
17
17
|
import { yellow } from './colors';
|
|
18
|
-
import
|
|
18
|
+
import SkillsBubble from './components/SkillsBubble';
|
|
19
|
+
import SkillsValidation from './components/SkillsValidation';
|
|
19
20
|
import CategoryBars from './components/CategoryBars';
|
|
20
21
|
import SkillsAppendixTable from './components/SkillsAppendixTable';
|
|
21
22
|
import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
@@ -24,6 +25,8 @@ import { formatLocalDateTime } from './utils/date';
|
|
|
24
25
|
import ChatWidget from './chat/ChatWidget';
|
|
25
26
|
import UseCases from './components/UseCases';
|
|
26
27
|
import SummaryCards from './components/SummaryCards';
|
|
28
|
+
import GaugeCard from './components/GaugeCard';
|
|
29
|
+
import RiskCard from './components/RiskCard';
|
|
27
30
|
import TopContributingFactors from './components/TopContributingFactors';
|
|
28
31
|
import AiUsageBody from './components/AiUsageBody';
|
|
29
32
|
import SanctionsMatches from './components/SanctionsMatches';
|
|
@@ -204,28 +207,93 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
204
207
|
const OverviewSection = () => (
|
|
205
208
|
<div className={`${wrapperMaxWidth} mx-auto mt-6`}>
|
|
206
209
|
<Reveal headless={isHeadless} offsetY={8} durationMs={500}>
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
210
|
+
<div className={'rounded-xl shadow-xl p-6 sm:p-8 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
211
|
+
{/* Quadrant layout */}
|
|
212
|
+
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
|
213
|
+
{/* Top-left: Name, countries, badge image handled by ReportHeader */}
|
|
214
|
+
<div>
|
|
215
|
+
<ReportHeader
|
|
216
|
+
badgeId={badgeId}
|
|
217
|
+
developerName={badgeData.developerName}
|
|
218
|
+
updatedAt={updatedAt}
|
|
219
|
+
score={overallFinalPercent || 0}
|
|
220
|
+
isPublic={true}
|
|
221
|
+
badgeImageUrl={badgeData.badgeImageUrl || ''}
|
|
222
|
+
summary={undefined}
|
|
223
|
+
enterpriseMatch={null}
|
|
224
|
+
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
225
|
+
accountAuthenticity={assessmentResult?.account_authenticity}
|
|
226
|
+
companyName={badgeData.companyName}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
{/* Top-right: Role match section */}
|
|
230
|
+
<div className={'rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
231
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Role Alignment</div>
|
|
232
|
+
{(() => {
|
|
233
|
+
const em = assessmentResult?.enterprise_match;
|
|
234
|
+
if (!em) return <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>No role match available.</div>;
|
|
235
|
+
const role = em.role || {};
|
|
236
|
+
return (
|
|
237
|
+
<div className={'space-y-2'}>
|
|
238
|
+
<div className={'flex items-center gap-2'}>
|
|
239
|
+
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{role?.name || 'Role'}</div>
|
|
240
|
+
<span className={'px-2 py-0.5 rounded text-xs font-semibold'} style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>{em.label}</span>
|
|
241
|
+
</div>
|
|
242
|
+
{em.description ? (
|
|
243
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{em.description}</div>
|
|
244
|
+
) : null}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
})()}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Bottom-left: Technical score */}
|
|
251
|
+
<div>
|
|
252
|
+
{(() => {
|
|
253
|
+
const uiTech = graphInsights?.uiSummary?.technical || {};
|
|
254
|
+
const techPct = Math.round(Number(uiTech?.percent ?? 0));
|
|
255
|
+
const techLabel = uiTech?.label || 'EVIDENCE';
|
|
256
|
+
return (
|
|
257
|
+
<GaugeCard
|
|
258
|
+
title={'KYD Technical'}
|
|
259
|
+
description={'Composite of technical evidence; more right indicates stronger capability'}
|
|
260
|
+
percent={techPct}
|
|
261
|
+
label={techLabel}
|
|
262
|
+
topMovers={[]}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
})()}
|
|
266
|
+
</div>
|
|
267
|
+
{/* Bottom-right: Risk score */}
|
|
268
|
+
<div>
|
|
269
|
+
{(() => {
|
|
270
|
+
const uiRisk = graphInsights?.uiSummary?.risk || {};
|
|
271
|
+
const riskPctGood = Math.round(Number(uiRisk?.percent_good ?? 0));
|
|
272
|
+
const riskLabel = uiRisk?.label || 'RISK';
|
|
273
|
+
return (
|
|
274
|
+
<RiskCard
|
|
275
|
+
title={'KYD Risk'}
|
|
276
|
+
description={'Lower bar height indicates lower risk exposure'}
|
|
277
|
+
percentGood={riskPctGood}
|
|
278
|
+
label={riskLabel}
|
|
279
|
+
topMovers={[]}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
})()}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
224
286
|
</Reveal>
|
|
287
|
+
|
|
288
|
+
{/* Full-width Skills bubble chart below quadrant */}
|
|
225
289
|
<div className={'rounded-xl shadow-xl p-6 sm:p-8 mt-6 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
226
|
-
<Reveal headless={isHeadless}
|
|
227
|
-
|
|
228
|
-
|
|
290
|
+
<Reveal headless={isHeadless}>
|
|
291
|
+
<div className={'kyd-avoid-break'}>
|
|
292
|
+
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
|
|
293
|
+
<SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
294
|
+
</div>
|
|
295
|
+
</Reveal>
|
|
296
|
+
<div className={'pt-6 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
229
297
|
Report Completed: {formatLocalDateTime(updatedAt)}
|
|
230
298
|
</div>
|
|
231
299
|
</div>
|
|
@@ -392,7 +460,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
392
460
|
<div className="pt-8 border-t kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
393
461
|
<h3 className={'text-xl font-bold mb-3 kyd-keep-with-next'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
394
462
|
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
395
|
-
<
|
|
463
|
+
<SkillsValidation skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
396
464
|
</div>
|
|
397
465
|
</div>
|
|
398
466
|
</Reveal>
|
|
@@ -49,12 +49,6 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
49
49
|
const tint = hexToRgba(pickTint(score || 0), 0.06);
|
|
50
50
|
const matchLabel = enterpriseMatch?.label;
|
|
51
51
|
|
|
52
|
-
const formattedDate = updatedAt ? formatLocalDate(updatedAt, {
|
|
53
|
-
year: 'numeric',
|
|
54
|
-
month: 'long',
|
|
55
|
-
day: 'numeric',
|
|
56
|
-
}) : 'N/A';
|
|
57
|
-
|
|
58
52
|
return (
|
|
59
53
|
<div
|
|
60
54
|
className={'mb-8 p-6 rounded-xl shadow-lg border'}
|
|
@@ -79,33 +73,15 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
79
73
|
</div>
|
|
80
74
|
);
|
|
81
75
|
})()}
|
|
82
|
-
<div className="flex flex-col
|
|
83
|
-
{/*
|
|
84
|
-
<div className="w-full
|
|
85
|
-
<div className=
|
|
86
|
-
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
|
|
87
|
-
{/* Centered overlay slightly lower on Y axis, responsive and readable */}
|
|
88
|
-
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
89
|
-
<div className="font-extrabold text-black text-3xl " >
|
|
90
|
-
{Math.round(score || 0)}%
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
{/* Right Half: Title, Candidate, Details and Summary section */}
|
|
97
|
-
<div className="w-full md:w-2/3">
|
|
98
|
-
<div className={'space-y-4'}>
|
|
76
|
+
<div className="flex flex-col gap-6">
|
|
77
|
+
{/* Info section: Title, Candidate, Details and Summary */}
|
|
78
|
+
<div className="w-full">
|
|
79
|
+
<div className='space-y-2'>
|
|
99
80
|
<span className='flex gap-2 w-full items-end text-start justify-start'>
|
|
100
81
|
<h2 className={'text-xl font-light'} style={{ color: 'var(--text-main)' }}>KYD Candidate Report:</h2>
|
|
101
82
|
<div className={'text-xl font-bold'} style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</div>
|
|
102
83
|
</span>
|
|
103
|
-
<div className={'text-sm
|
|
104
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Developer:</span> <span style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</span></p>
|
|
105
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Requested By:</span> <span style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</span></p>
|
|
106
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Organization:</span> <span style={{ color: 'var(--text-main)' }}>{companyName || 'Unaffiliated'}</span></p>
|
|
107
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Date Generated:</span> <span style={{ color: 'var(--text-main)' }}>{formattedDate}</span></p>
|
|
108
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Report ID:</span> <span style={{ color: 'var(--text-main)' }}>{badgeId || 'N/A'}</span></p>
|
|
84
|
+
<div className={'text-sm'}>
|
|
109
85
|
{Array.isArray(countries) && countries.length > 0 && (
|
|
110
86
|
(() => {
|
|
111
87
|
const countryNames = countries
|
|
@@ -155,6 +131,18 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
155
131
|
)}
|
|
156
132
|
</div>
|
|
157
133
|
</div>
|
|
134
|
+
|
|
135
|
+
{/* Badge Image with robust centered overlay */}
|
|
136
|
+
<div className="w-full flex items-center justify-center self-stretch">
|
|
137
|
+
<div className="relative w-full max-w-xs select-none">
|
|
138
|
+
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
|
|
139
|
+
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
140
|
+
<div className="font-extrabold text-black text-3xl ">
|
|
141
|
+
{Math.round(score || 0)}%
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
158
146
|
</div>
|
|
159
147
|
</div>
|
|
160
148
|
);
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
YAxis,
|
|
12
12
|
CartesianGrid,
|
|
13
13
|
Tooltip,
|
|
14
|
+
ScatterChart,
|
|
15
|
+
Scatter,
|
|
16
|
+
ZAxis,
|
|
14
17
|
} from 'recharts';
|
|
15
18
|
|
|
16
19
|
|
|
@@ -265,69 +268,31 @@ const Skills = ({ skillsCategoryRadar, headless }: { skillsMatrix?: SkillsMatrix
|
|
|
265
268
|
}}
|
|
266
269
|
>
|
|
267
270
|
{(() => {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
const data = combinedBubbleData.map((d, idx) => ({ x: (idx % 4) + 1, y: Math.floor(idx / 4) + 1, value: d.value, label: d.label }));
|
|
272
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
|
+
const BubbleTooltip = ({ active, payload }: any) => {
|
|
274
|
+
if (!active || !payload || !payload.length) return null;
|
|
275
|
+
const p = payload[0]?.payload;
|
|
276
|
+
return (
|
|
277
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
278
|
+
<div style={{ fontWeight: 600 }}>{p?.label}</div>
|
|
279
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>Score: {p?.value}</div>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
275
283
|
return (
|
|
276
|
-
<
|
|
277
|
-
<
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<g
|
|
287
|
-
key={idx}
|
|
288
|
-
transform={`translate(${b.x},${b.y})`}
|
|
289
|
-
onMouseEnter={(e) => {
|
|
290
|
-
const rect = containerRef.current?.getBoundingClientRect();
|
|
291
|
-
if (!rect) return;
|
|
292
|
-
setFootprintChartTooltip({
|
|
293
|
-
visible: true,
|
|
294
|
-
x: e.clientX - rect.left + 12,
|
|
295
|
-
y: e.clientY - rect.top + 12,
|
|
296
|
-
title: b.label,
|
|
297
|
-
body: `Score: ${b.value}`,
|
|
298
|
-
});
|
|
299
|
-
}}
|
|
300
|
-
onMouseMove={(e) => {
|
|
301
|
-
if (!footprintChartTooltip || !containerRef.current) return;
|
|
302
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
303
|
-
setFootprintChartTooltip({ ...footprintChartTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
|
|
304
|
-
}}
|
|
305
|
-
onMouseLeave={() => setFootprintChartTooltip(null)}
|
|
306
|
-
>
|
|
307
|
-
<defs>
|
|
308
|
-
<clipPath id={clipId}>
|
|
309
|
-
<circle r={b.r} />
|
|
310
|
-
</clipPath>
|
|
311
|
-
</defs>
|
|
312
|
-
<circle r={b.r} fill={'var(--bubble-foreground)'} stroke={'var(--icon-button-secondary)'} />
|
|
313
|
-
<title>{`${b.label}: ${b.value}`}</title>
|
|
314
|
-
{canShowText && labelSpec.lines.length > 0 ? (
|
|
315
|
-
<g clipPath={`url(#${clipId})`} style={{ pointerEvents: 'none' }}>
|
|
316
|
-
{labelSpec.lines.map((line, li) => (
|
|
317
|
-
<text key={li} y={startY + li * (labelSpec.fontSize + lineGap)} fontSize={labelSpec.fontSize} textAnchor="middle" fill={'var(--bubble-background)'}>
|
|
318
|
-
{line}
|
|
319
|
-
</text>
|
|
320
|
-
))}
|
|
321
|
-
</g>
|
|
322
|
-
) : null}
|
|
323
|
-
</g>
|
|
324
|
-
);
|
|
325
|
-
})}
|
|
326
|
-
</g>
|
|
327
|
-
</svg>
|
|
284
|
+
<ResponsiveContainer>
|
|
285
|
+
<ScatterChart margin={{ top: 8, right: 8, bottom: 8, left: 8 }}>
|
|
286
|
+
<CartesianGrid strokeDasharray="3 3" stroke={'var(--icon-button-secondary)'} />
|
|
287
|
+
<XAxis type="number" dataKey="x" tick={false} axisLine={false} domain={[0, 5]} />
|
|
288
|
+
<YAxis type="number" dataKey="y" tick={false} axisLine={false} domain={[0, Math.ceil(data.length / 4) + 1]} />
|
|
289
|
+
<ZAxis dataKey="value" range={[80, 360]} />
|
|
290
|
+
<Tooltip content={<BubbleTooltip />} cursor={{ stroke: 'var(--icon-button-secondary)' }} />
|
|
291
|
+
<Scatter data={data} fill={'var(--bubble-foreground)'} />
|
|
292
|
+
</ScatterChart>
|
|
293
|
+
</ResponsiveContainer>
|
|
328
294
|
);
|
|
329
295
|
})()}
|
|
330
|
-
{!headless && <TooltipBox state={footprintChartTooltip} />}
|
|
331
296
|
</div>
|
|
332
297
|
{/* Legend */}
|
|
333
298
|
<div className={'mt-3'}>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
|
4
|
+
import BubbleChart from '@knowyourdeveloper/react-bubble-chart';
|
|
5
|
+
|
|
6
|
+
type SkillsRadarPoint = {
|
|
7
|
+
axis: string;
|
|
8
|
+
observed?: number;
|
|
9
|
+
self_reported?: number;
|
|
10
|
+
certified?: number;
|
|
11
|
+
experience?: number; // 0-100 saturation driver
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type HoverTooltipState = {
|
|
15
|
+
visible: boolean;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
title: string;
|
|
19
|
+
body?: string;
|
|
20
|
+
} | null;
|
|
21
|
+
|
|
22
|
+
const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
23
|
+
if (!state || !state.visible) return null;
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
style={{
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
left: state.x,
|
|
29
|
+
top: state.y,
|
|
30
|
+
pointerEvents: 'none',
|
|
31
|
+
background: 'var(--content-card-background)',
|
|
32
|
+
border: '1px solid var(--icon-button-secondary)',
|
|
33
|
+
color: 'var(--text-main)',
|
|
34
|
+
padding: 10,
|
|
35
|
+
borderRadius: 6,
|
|
36
|
+
minWidth: 250,
|
|
37
|
+
maxWidth: 320,
|
|
38
|
+
zIndex: 10,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div style={{ fontWeight: 600 }}>{state.title}</div>
|
|
42
|
+
{state.body ? (
|
|
43
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{state.body}</div>
|
|
44
|
+
) : null}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
|
|
50
|
+
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
51
|
+
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
52
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const legendRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof window !== 'undefined') {
|
|
58
|
+
const id = window.setTimeout(() => {
|
|
59
|
+
try { window.dispatchEvent(new Event('resize')); } catch {}
|
|
60
|
+
}, 0);
|
|
61
|
+
return () => window.clearTimeout(id);
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
// ratio drives size: average of observed/self_reported/certified
|
|
66
|
+
const bubbles = useMemo(() => {
|
|
67
|
+
const seriesAvg = (d: SkillsRadarPoint): number => {
|
|
68
|
+
const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
|
|
69
|
+
const nonZero = vals.filter((v) => v > 0);
|
|
70
|
+
const base = (nonZero.length > 0 ? nonZero : vals);
|
|
71
|
+
return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const maxValue = Math.max(1, ...skillsRadarLimited.map(seriesAvg));
|
|
75
|
+
|
|
76
|
+
return skillsRadarLimited.map((d) => {
|
|
77
|
+
const value = seriesAvg(d);
|
|
78
|
+
const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
|
|
79
|
+
const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
|
|
80
|
+
// map experience to saturation in HSL; keep hue constant for brand-neutral look
|
|
81
|
+
const saturation = Math.max(20, Math.min(100, experience));
|
|
82
|
+
const color = `hsl(210 ${saturation}% 45%)`;
|
|
83
|
+
return {
|
|
84
|
+
label: d.axis,
|
|
85
|
+
value: size,
|
|
86
|
+
color,
|
|
87
|
+
tooltip: `${d.axis}\nRatio: ${value}\nExperience: ${experience}`,
|
|
88
|
+
data: { ratio: value, experience }
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}, [skillsRadarLimited]);
|
|
92
|
+
|
|
93
|
+
const percentLegend = useMemo(() => {
|
|
94
|
+
const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
|
|
95
|
+
return bubbles
|
|
96
|
+
.map((b) => ({ label: b.label, percent: total > 0 ? Math.round(((b.data?.ratio || 0) / total) * 100) : 0, experience: b.data?.experience || 0 }))
|
|
97
|
+
.sort((a, b) => b.percent - a.percent);
|
|
98
|
+
}, [bubbles]);
|
|
99
|
+
|
|
100
|
+
if (!hasRadar) return null;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className={'kyd-avoid-break'}>
|
|
104
|
+
<div ref={containerRef} style={{ width: '100%', height: 340 }}>
|
|
105
|
+
<BubbleChart
|
|
106
|
+
width={'100%'}
|
|
107
|
+
height={'100%'}
|
|
108
|
+
graph={{ zoom: 1 }}
|
|
109
|
+
showLegend={false}
|
|
110
|
+
data={bubbles}
|
|
111
|
+
legend={false}
|
|
112
|
+
valueFont={{ family: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, sans-serif', size: 11, color: 'var(--text-main)' }}
|
|
113
|
+
labelFont={{ family: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, sans-serif', size: 12, color: 'var(--text-main)' }}
|
|
114
|
+
bubbleClickFun={(_evt: unknown, b: { label?: string }) => {
|
|
115
|
+
try {
|
|
116
|
+
if (typeof window !== 'undefined') {
|
|
117
|
+
const anchor = `#appendix-skills-cat-${encodeURIComponent(String(b?.label || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
|
|
118
|
+
const url = `#appendix${anchor}`;
|
|
119
|
+
window.location.hash = url;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}}
|
|
123
|
+
tooltip
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
<div className={'mt-3'}>
|
|
127
|
+
<div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
128
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
|
|
129
|
+
{percentLegend.map((item, idx) => (
|
|
130
|
+
<button
|
|
131
|
+
key={idx}
|
|
132
|
+
className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
|
|
133
|
+
style={{ color: 'var(--text-secondary)', background: 'transparent' }}
|
|
134
|
+
onClick={() => {
|
|
135
|
+
try {
|
|
136
|
+
if (typeof window !== 'undefined') {
|
|
137
|
+
const anchor = `#appendix-skills-cat-${encodeURIComponent(item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
|
|
138
|
+
const url = `#appendix${anchor}`;
|
|
139
|
+
window.location.hash = url;
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
}}
|
|
143
|
+
onMouseEnter={(e) => {
|
|
144
|
+
const rect = legendRef.current?.getBoundingClientRect();
|
|
145
|
+
if (!rect) return;
|
|
146
|
+
const x = e.clientX - rect.left + 12;
|
|
147
|
+
const y = e.clientY - rect.top + 12;
|
|
148
|
+
setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} • ${item.percent}% of ratio • Experience ${item.experience}` });
|
|
149
|
+
}}
|
|
150
|
+
onMouseMove={(e) => {
|
|
151
|
+
if (!legendTooltip || !legendRef.current) return;
|
|
152
|
+
const rect = legendRef.current.getBoundingClientRect();
|
|
153
|
+
setLegendTooltip({ ...legendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
|
|
154
|
+
}}
|
|
155
|
+
onMouseLeave={() => setLegendTooltip(null)}
|
|
156
|
+
>
|
|
157
|
+
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: `hsl(210 ${Math.max(20, Math.min(100, item.experience))}% 45%)`, flexShrink: 0 }} />
|
|
158
|
+
<span className="truncate">{item.label}</span>
|
|
159
|
+
<span className="ml-auto opacity-80">{item.percent}%</span>
|
|
160
|
+
</button>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
{!headless && <TooltipBox state={legendTooltip} />}
|
|
164
|
+
{/* Legends */}
|
|
165
|
+
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
166
|
+
<div className="flex items-center gap-2">
|
|
167
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 80% 45%)' }} />
|
|
168
|
+
<span>Bubble size: relative ratio of observed/self-reported/certified</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 20% 45%)' }} />
|
|
172
|
+
<span>Color saturation: experience (darker = more experienced)</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
|
|
5
|
+
|
|
6
|
+
type SkillsRadarPoint = {
|
|
7
|
+
axis: string;
|
|
8
|
+
observed?: number;
|
|
9
|
+
self_reported?: number;
|
|
10
|
+
certified?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function SkillsValidation({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
|
|
14
|
+
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 8);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<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 }}>
|
|
18
|
+
<h4 className={'font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills by Validation Type</h4>
|
|
19
|
+
<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>
|
|
20
|
+
<div style={{ height: 360 }}>
|
|
21
|
+
<ResponsiveContainer>
|
|
22
|
+
<BarChart data={skillsRadarLimited} margin={{ top: 8, right: 8, left: 8, bottom: 36 }}>
|
|
23
|
+
<CartesianGrid strokeDasharray="3 3" stroke={'var(--icon-button-secondary)'} />
|
|
24
|
+
<XAxis dataKey="axis" tick={{ fill: 'var(--text-secondary)', fontSize: 12 }} interval={0} angle={-20} textAnchor="end" height={50} />
|
|
25
|
+
<YAxis domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} />
|
|
26
|
+
<Tooltip contentStyle={{ background: 'var(--content-card-background)', border: `1px solid var(--icon-button-secondary)`, color: 'var(--text-main)' }} />
|
|
27
|
+
<Bar dataKey="observed" name="Observed" fill={'var(--bar-observed)'} isAnimationActive={!headless} />
|
|
28
|
+
<Bar dataKey="self_reported" name="Self-reported" fill={'var(--bar-self-reported)'} isAnimationActive={!headless} />
|
|
29
|
+
<Bar dataKey="certified" name="Certified" fill={'var(--bar-certified)'} isAnimationActive={!headless} />
|
|
30
|
+
</BarChart>
|
|
31
|
+
</ResponsiveContainer>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
@@ -20,12 +20,10 @@ export default function SummaryCards({ graphInsights, assessmentResult, topBusin
|
|
|
20
20
|
const riskLabel = uiRisk?.label || 'RISK';
|
|
21
21
|
const riskTop = uiRisk?.top_movers && uiRisk.top_movers.length > 0 ? uiRisk.top_movers : topBusinessForGenre('Risk');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const aiLabel = 'AI Transparency';
|
|
25
|
-
const aiTopMovers = ai?.key_findings || [];
|
|
23
|
+
// AI section removed per requirements
|
|
26
24
|
|
|
27
25
|
return (
|
|
28
|
-
<div className="grid grid-cols-1 sm:grid-cols-
|
|
26
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 *:min-h-full">
|
|
29
27
|
<GaugeCard
|
|
30
28
|
key={'technical-card'}
|
|
31
29
|
title={'KYD Technical'}
|
|
@@ -44,15 +42,7 @@ export default function SummaryCards({ graphInsights, assessmentResult, topBusin
|
|
|
44
42
|
topMoversTitle={'Top Score Movers'}
|
|
45
43
|
tooltipText={'Higher bar filled indicates lower overall risk; movement to the right reflects improved risk posture.'}
|
|
46
44
|
/>
|
|
47
|
-
|
|
48
|
-
key={'ai-card'}
|
|
49
|
-
title={'KYD AI (Beta)'}
|
|
50
|
-
description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
|
|
51
|
-
percent={ai?.transparency_score}
|
|
52
|
-
label={aiLabel}
|
|
53
|
-
topMovers={(aiTopMovers).map(t => ({ label: t, uid: 'ai-usage' }))}
|
|
54
|
-
topMoversTitle={'Key Findings'}
|
|
55
|
-
/>
|
|
45
|
+
{/* AI card removed */}
|
|
56
46
|
</div>
|
|
57
47
|
);
|
|
58
48
|
}
|
|
@@ -3,7 +3,7 @@ import { useRouter } from 'next/navigation';
|
|
|
3
3
|
import { ProviderIcon } from '../utils/provider';
|
|
4
4
|
import { normalizeLinkedInInput } from './linkedin';
|
|
5
5
|
import type { ConnectAccountsProps } from './types';
|
|
6
|
-
import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings } from 'lucide-react';
|
|
6
|
+
import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings, Shield, InfoIcon } from 'lucide-react';
|
|
7
7
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
8
8
|
import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
|
|
9
9
|
import Link from 'next/link';
|
|
@@ -38,7 +38,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
38
38
|
headerDescription,
|
|
39
39
|
requiredProviders,
|
|
40
40
|
companyName,
|
|
41
|
-
initialProviderId,
|
|
41
|
+
initialProviderId, // = process.env.NEXT_PUBLIC_STAGE === 'dev' ? 'githubapp' : undefined,
|
|
42
42
|
githubAppSlugId,
|
|
43
43
|
userId,
|
|
44
44
|
inviteId,
|
|
@@ -50,6 +50,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
50
50
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
51
51
|
const [isDisconnecting, setIsDisconnecting] = useState<string | null>(null);
|
|
52
52
|
const [showGithubManage, setShowGithubManage] = useState(false);
|
|
53
|
+
const [showDataHandling, setShowDataHandling] = useState(false);
|
|
53
54
|
|
|
54
55
|
const apiBase = apiGatewayUrl || (typeof process !== 'undefined' ? (process.env.NEXT_PUBLIC_API_GATEWAY_URL as string) : '');
|
|
55
56
|
const connectedIds = useMemo(
|
|
@@ -197,6 +198,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
197
198
|
setSelectedProviderIdAndCallback(null);
|
|
198
199
|
setLinkUrl('');
|
|
199
200
|
setShowGithubManage(false);
|
|
201
|
+
setShowDataHandling(false);
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
const cardVariants = {
|
|
@@ -204,6 +206,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
204
206
|
animate: { opacity: 1, y: 0 },
|
|
205
207
|
exit: { opacity: 0, y: -20 },
|
|
206
208
|
};
|
|
209
|
+
const fadeOnly = {
|
|
210
|
+
initial: { opacity: 0 },
|
|
211
|
+
animate: { opacity: 1 },
|
|
212
|
+
exit: { opacity: 0 },
|
|
213
|
+
};
|
|
207
214
|
|
|
208
215
|
// GitHub status helpers
|
|
209
216
|
const githubConnectedAccount = useMemo(() => {
|
|
@@ -216,14 +223,189 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
216
223
|
}, [githubConnectedAccount]);
|
|
217
224
|
|
|
218
225
|
return (
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
<
|
|
226
|
+
<AnimatePresence initial={false} mode="wait">
|
|
227
|
+
{showDataHandling ? (
|
|
228
|
+
<motion.div
|
|
229
|
+
key="data-handling-card"
|
|
230
|
+
className="rounded-xl border max-w-xl w-full"
|
|
231
|
+
initial="initial" animate="animate" exit="exit" variants={fadeOnly}
|
|
232
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
233
|
+
>
|
|
234
|
+
<div className="sm:p-6 p-4">
|
|
235
|
+
<button onClick={() => setShowDataHandling(false)} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
236
|
+
<ArrowLeft className="w-4 h-4" />
|
|
237
|
+
Back
|
|
238
|
+
</button>
|
|
239
|
+
<div className="text-center">
|
|
240
|
+
<div className="flex justify-center mb-4">
|
|
241
|
+
<InfoIcon className="w-10 h-10 text-[var(--text-main)]" />
|
|
242
|
+
</div>
|
|
243
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>How your data is handled</h3>
|
|
244
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-2 leading-relaxed">
|
|
245
|
+
Hey there! We understand that giving access to your private repositories can be a bit scary. So here's the deal: We install the KYD GitHub App in your account - we only have read access. Then, once you request a badge assessment, we read the repositories and analyze the code, then its deleted, forever. Your code is not accessible to anyone, not even us.
|
|
246
|
+
</p>
|
|
247
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-3 leading-relaxed">
|
|
248
|
+
For details, see our{' '}
|
|
249
|
+
<Link href="https://www.knowyourdeveloper.ai/privacy-policy" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>
|
|
250
|
+
Privacy Policy <ExternalLink className="size-3 inline-block ml-1" />
|
|
251
|
+
</Link>.
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</motion.div>
|
|
256
|
+
) : selectedProvider && selectedProvider.id !== 'githubapp' ? (
|
|
257
|
+
<motion.div
|
|
258
|
+
key="connect-card"
|
|
259
|
+
initial="initial" animate="animate" exit="exit" variants={fadeOnly}
|
|
260
|
+
className="rounded-xl border max-w-xl w-full"
|
|
261
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
262
|
+
>
|
|
263
|
+
<div className="sm:p-6 p-4">
|
|
264
|
+
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
265
|
+
<ArrowLeft className="w-4 h-4" />
|
|
266
|
+
Back
|
|
267
|
+
</button>
|
|
268
|
+
<div className="text-center">
|
|
269
|
+
<div className="flex justify-center mb-4">
|
|
270
|
+
<ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
|
|
271
|
+
</div>
|
|
272
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
273
|
+
{selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
|
|
274
|
+
? `Use Public ${selectedProvider.name} Profile`
|
|
275
|
+
: `Connect ${selectedProvider.name}`}
|
|
276
|
+
</h3>
|
|
277
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
278
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
279
|
+
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
280
|
+
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
|
|
285
|
+
<motion.form
|
|
286
|
+
onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
|
|
287
|
+
className="mt-6 space-y-4"
|
|
288
|
+
initial="initial" animate="animate" exit="exit" variants={cardVariants}
|
|
289
|
+
>
|
|
290
|
+
{selectedProvider.id === 'linkedin' && (
|
|
291
|
+
<p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
|
|
292
|
+
<Link
|
|
293
|
+
href="https://www.linkedin.com/public-profile/settings"
|
|
294
|
+
target="_blank"
|
|
295
|
+
rel="noopener noreferrer"
|
|
296
|
+
className="underline"
|
|
297
|
+
style={{ color: 'var(--icon-accent)' }}
|
|
298
|
+
>
|
|
299
|
+
LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
|
|
300
|
+
</Link>
|
|
301
|
+
. This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
|
|
302
|
+
</p>
|
|
303
|
+
)}
|
|
304
|
+
<div className="relative">
|
|
305
|
+
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
306
|
+
<Input
|
|
307
|
+
type="url"
|
|
308
|
+
value={linkUrl}
|
|
309
|
+
onChange={(e) => setLinkUrl(e.target.value)}
|
|
310
|
+
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
311
|
+
required
|
|
312
|
+
className="w-full border bg-transparent p-2 pl-9"
|
|
313
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
314
|
+
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
315
|
+
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
319
|
+
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
320
|
+
{isSubmitting ? (
|
|
321
|
+
<div className="flex items-center justify-center">
|
|
322
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
323
|
+
Connecting...
|
|
324
|
+
</div>
|
|
325
|
+
) : (
|
|
326
|
+
'Connect'
|
|
327
|
+
)}
|
|
328
|
+
</Button>
|
|
329
|
+
</motion.div>
|
|
330
|
+
</motion.form>
|
|
331
|
+
) : (
|
|
332
|
+
<div className="mt-6">
|
|
333
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
334
|
+
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
335
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
336
|
+
Connect with {selectedProvider.name}
|
|
337
|
+
</Button>
|
|
338
|
+
</motion.div>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
</motion.div>
|
|
343
|
+
) : selectedProvider && selectedProvider.id === 'githubapp' ? (
|
|
344
|
+
(!showGithubManage && initialProviderId === 'githubapp') ? (
|
|
345
|
+
<div
|
|
346
|
+
key="github-card"
|
|
347
|
+
className="rounded-xl border max-w-xl w-full"
|
|
348
|
+
style={{
|
|
349
|
+
backgroundColor: 'var(--content-card-background)',
|
|
350
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
<motion.div className="p-6 flex flex-col items-center" initial="initial" animate="animate" exit="exit" variants={fadeOnly}>
|
|
354
|
+
<div className="w-full flex items-center gap-3 mb-2 justify-center">
|
|
355
|
+
<ProviderIcon name="github" className="w-8 h-8 inline-block" />
|
|
356
|
+
<span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
|
|
357
|
+
</div>
|
|
358
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
|
|
359
|
+
You've successfully linked your GitHub account!
|
|
360
|
+
<br />
|
|
361
|
+
To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you'd like to highlight private work or share additional contributions for verification.
|
|
362
|
+
<br /><br />
|
|
363
|
+
<span className="text-[var(--text-main)] font-medium">
|
|
364
|
+
Would you like to connect your private repositories?
|
|
365
|
+
</span>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onClick={() => setShowDataHandling(true)}
|
|
369
|
+
className="sm:text-sm text-xs underline text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)]"
|
|
370
|
+
>
|
|
371
|
+
See how your data is handled
|
|
372
|
+
</button>
|
|
373
|
+
</p>
|
|
374
|
+
<div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
|
|
375
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
376
|
+
<Button
|
|
377
|
+
className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
|
|
378
|
+
variant="destructive"
|
|
379
|
+
onClick={() => {
|
|
380
|
+
handleConnectBack();
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
No, don't connect
|
|
384
|
+
</Button>
|
|
385
|
+
</motion.div>
|
|
386
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
387
|
+
<Button
|
|
388
|
+
className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
|
|
389
|
+
onClick={onGithubAppInstall}
|
|
390
|
+
>
|
|
391
|
+
<span className="flex items-center justify-center">
|
|
392
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
393
|
+
Yes, connect my private repos
|
|
394
|
+
</span>
|
|
395
|
+
</Button>
|
|
396
|
+
</motion.div>
|
|
397
|
+
</div>
|
|
398
|
+
</motion.div>
|
|
399
|
+
</div>
|
|
400
|
+
) : (
|
|
222
401
|
<motion.div
|
|
223
|
-
key="
|
|
402
|
+
key="github-manage"
|
|
224
403
|
className="rounded-xl border max-w-xl w-full"
|
|
225
404
|
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
226
|
-
|
|
405
|
+
variants={cardVariants}
|
|
406
|
+
initial="initial"
|
|
407
|
+
animate="animate"
|
|
408
|
+
exit="exit"
|
|
227
409
|
>
|
|
228
410
|
<div className="sm:p-6 p-4">
|
|
229
411
|
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
@@ -232,227 +414,85 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
232
414
|
</button>
|
|
233
415
|
<div className="text-center">
|
|
234
416
|
<div className="flex justify-center mb-4">
|
|
235
|
-
<ProviderIcon name=
|
|
417
|
+
<ProviderIcon name="github" className="w-10 h-10" />
|
|
236
418
|
</div>
|
|
237
|
-
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
: `Connect ${selectedProvider.name}`}
|
|
241
|
-
</h3>
|
|
242
|
-
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
243
|
-
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
244
|
-
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
245
|
-
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
419
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>Manage GitHub Connections</h3>
|
|
420
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md">
|
|
421
|
+
Connect or disconnect your GitHub OAuth account and optional GitHub App for private repositories.
|
|
246
422
|
</p>
|
|
247
423
|
</div>
|
|
248
424
|
|
|
249
|
-
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)}
|
|
269
|
-
<div className="relative">
|
|
270
|
-
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
271
|
-
<Input
|
|
272
|
-
type="url"
|
|
273
|
-
value={linkUrl}
|
|
274
|
-
onChange={(e) => setLinkUrl(e.target.value)}
|
|
275
|
-
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
276
|
-
required
|
|
277
|
-
className="w-full border bg-transparent p-2 pl-9"
|
|
278
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
279
|
-
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
280
|
-
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
281
|
-
/>
|
|
282
|
-
</div>
|
|
283
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
284
|
-
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
285
|
-
{isSubmitting ? (
|
|
286
|
-
<div className="flex items-center justify-center">
|
|
287
|
-
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
288
|
-
Connecting...
|
|
289
|
-
</div>
|
|
425
|
+
<div className="mt-6 space-y-4">
|
|
426
|
+
<div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
427
|
+
<div className="flex items-center justify-between">
|
|
428
|
+
<div>
|
|
429
|
+
<div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub OAuth</div>
|
|
430
|
+
<div className="sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>{isGithubConnected ? 'Connected' : 'Not connected'}</div>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="flex items-center gap-2">
|
|
433
|
+
{isGithubConnected ? (
|
|
434
|
+
<Button
|
|
435
|
+
onClick={() => onDisconnect('github')}
|
|
436
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
437
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
438
|
+
variant="destructive"
|
|
439
|
+
disabled={isDisconnecting === 'github'}
|
|
440
|
+
>
|
|
441
|
+
{isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
|
|
442
|
+
<span>Disconnect</span>
|
|
443
|
+
</Button>
|
|
290
444
|
) : (
|
|
291
|
-
|
|
445
|
+
<Button
|
|
446
|
+
onClick={() => onOAuth('github')}
|
|
447
|
+
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
448
|
+
>
|
|
449
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
450
|
+
Connect
|
|
451
|
+
</Button>
|
|
292
452
|
)}
|
|
293
|
-
</
|
|
294
|
-
</motion.div>
|
|
295
|
-
</motion.form>
|
|
296
|
-
) : (
|
|
297
|
-
<div className="mt-6">
|
|
298
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
299
|
-
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
300
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
301
|
-
Connect with {selectedProvider.name}
|
|
302
|
-
</Button>
|
|
303
|
-
</motion.div>
|
|
304
|
-
</div>
|
|
305
|
-
)}
|
|
306
|
-
</div>
|
|
307
|
-
</motion.div>
|
|
308
|
-
</AnimatePresence>
|
|
309
|
-
) : selectedProvider && selectedProvider.id === 'githubapp' ? (
|
|
310
|
-
<AnimatePresence>
|
|
311
|
-
{(!showGithubManage && initialProviderId === 'githubapp') ? (
|
|
312
|
-
<motion.div
|
|
313
|
-
key="github-card"
|
|
314
|
-
className="rounded-xl border max-w-xl w-full"
|
|
315
|
-
style={{
|
|
316
|
-
backgroundColor: 'var(--content-card-background)',
|
|
317
|
-
borderColor: 'var(--icon-button-secondary)',
|
|
318
|
-
}}
|
|
319
|
-
variants={cardVariants}
|
|
320
|
-
initial="initial"
|
|
321
|
-
animate="animate"
|
|
322
|
-
exit="exit"
|
|
323
|
-
transition={{ duration: 0.3 }}
|
|
324
|
-
>
|
|
325
|
-
<div className="p-6 flex flex-col items-center">
|
|
326
|
-
<div className="w-full flex items-center gap-3 mb-2 justify-center">
|
|
327
|
-
<ProviderIcon name="github" className="w-8 h-8 inline-block" />
|
|
328
|
-
<span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
|
|
329
|
-
</div>
|
|
330
|
-
<p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
|
|
331
|
-
You’ve successfully linked your GitHub account!
|
|
332
|
-
<br />
|
|
333
|
-
To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you’d like to highlight private work or share additional contributions for verification.
|
|
334
|
-
<br /><br />
|
|
335
|
-
<span className="text-[var(--text-main)] font-medium">
|
|
336
|
-
Would you like to connect your private repositories?
|
|
337
|
-
</span>
|
|
338
|
-
</p>
|
|
339
|
-
<div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
|
|
340
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
341
|
-
<Button
|
|
342
|
-
className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
|
|
343
|
-
variant="destructive"
|
|
344
|
-
onClick={() => {
|
|
345
|
-
handleConnectBack();
|
|
346
|
-
}}
|
|
347
|
-
>
|
|
348
|
-
No, don't connect
|
|
349
|
-
</Button>
|
|
350
|
-
</motion.div>
|
|
351
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
352
|
-
<Button
|
|
353
|
-
className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
|
|
354
|
-
onClick={onGithubAppInstall}
|
|
355
|
-
>
|
|
356
|
-
<span className="flex items-center justify-center">
|
|
357
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
358
|
-
Yes, connect my private repos
|
|
359
|
-
</span>
|
|
360
|
-
</Button>
|
|
361
|
-
</motion.div>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
</motion.div>
|
|
365
|
-
) : (
|
|
366
|
-
<motion.div
|
|
367
|
-
key="github-manage"
|
|
368
|
-
className="rounded-xl border max-w-xl w-full"
|
|
369
|
-
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
370
|
-
variants={cardVariants}
|
|
371
|
-
initial="initial"
|
|
372
|
-
animate="animate"
|
|
373
|
-
exit="exit"
|
|
374
|
-
>
|
|
375
|
-
<div className="sm:p-6 p-4">
|
|
376
|
-
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
377
|
-
<ArrowLeft className="w-4 h-4" />
|
|
378
|
-
Back
|
|
379
|
-
</button>
|
|
380
|
-
<div className="text-center">
|
|
381
|
-
<div className="flex justify-center mb-4">
|
|
382
|
-
<ProviderIcon name="github" className="w-10 h-10" />
|
|
453
|
+
</div>
|
|
383
454
|
</div>
|
|
384
|
-
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>Manage GitHub Connections</h3>
|
|
385
|
-
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md">
|
|
386
|
-
Connect or disconnect your GitHub OAuth account and optional GitHub App for private repositories.
|
|
387
|
-
</p>
|
|
388
455
|
</div>
|
|
389
456
|
|
|
390
|
-
<div className="
|
|
391
|
-
<div className="
|
|
392
|
-
<div
|
|
393
|
-
<div>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
402
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
403
|
-
variant="destructive"
|
|
404
|
-
disabled={isDisconnecting === 'github'}
|
|
405
|
-
>
|
|
406
|
-
{isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
|
|
407
|
-
<span>Disconnect</span>
|
|
408
|
-
</Button>
|
|
409
|
-
) : (
|
|
410
|
-
<Button
|
|
411
|
-
onClick={() => onOAuth('github')}
|
|
412
|
-
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
413
|
-
>
|
|
414
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
415
|
-
Connect
|
|
416
|
-
</Button>
|
|
417
|
-
)}
|
|
418
|
-
</div>
|
|
457
|
+
<div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
458
|
+
<div className="flex items-center justify-between">
|
|
459
|
+
<div>
|
|
460
|
+
<div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub App (Private Repos)</div>
|
|
461
|
+
<Button
|
|
462
|
+
className="inline-flex items-center text-xs font-medium text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)] transition-colors underline px-0 py-0 h-auto"
|
|
463
|
+
onClick={() => setShowDataHandling(true)}
|
|
464
|
+
variant="link"
|
|
465
|
+
>
|
|
466
|
+
How is my data handled?
|
|
467
|
+
</Button>
|
|
419
468
|
</div>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<Button
|
|
441
|
-
onClick={onGithubAppInstall}
|
|
442
|
-
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
443
|
-
>
|
|
444
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
445
|
-
Install
|
|
446
|
-
</Button>
|
|
447
|
-
)}
|
|
448
|
-
</div>
|
|
469
|
+
<div className="flex items-center gap-2">
|
|
470
|
+
{isGithubAppInstalled ? (
|
|
471
|
+
<Button
|
|
472
|
+
onClick={() => { window.location.href = 'https://github.com/settings/installations'; }}
|
|
473
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
474
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
475
|
+
variant="destructive"
|
|
476
|
+
>
|
|
477
|
+
<Unlink className="size-3 sm:size-4" />
|
|
478
|
+
<span>Uninstall</span>
|
|
479
|
+
</Button>
|
|
480
|
+
) : (
|
|
481
|
+
<Button
|
|
482
|
+
onClick={onGithubAppInstall}
|
|
483
|
+
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
484
|
+
>
|
|
485
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
486
|
+
Install
|
|
487
|
+
</Button>
|
|
488
|
+
)}
|
|
449
489
|
</div>
|
|
450
490
|
</div>
|
|
451
491
|
</div>
|
|
452
492
|
</div>
|
|
453
|
-
</
|
|
454
|
-
|
|
455
|
-
|
|
493
|
+
</div>
|
|
494
|
+
</motion.div>
|
|
495
|
+
)
|
|
456
496
|
) : (
|
|
457
497
|
<Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
|
|
458
498
|
<AnimatePresence mode="wait">
|
|
@@ -647,7 +687,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
647
687
|
</AnimatePresence>
|
|
648
688
|
</Card>
|
|
649
689
|
)}
|
|
650
|
-
|
|
690
|
+
</AnimatePresence>
|
|
651
691
|
);
|
|
652
692
|
}
|
|
653
693
|
|
package/src/types.ts
CHANGED
|
@@ -378,6 +378,8 @@ export interface GraphInsightsPayload {
|
|
|
378
378
|
observed?: number; // 0-100
|
|
379
379
|
self_reported?: number; // 0-100
|
|
380
380
|
certified?: number; // 0-100
|
|
381
|
+
// New: experience metric (0-100) for color saturation
|
|
382
|
+
experience?: number;
|
|
381
383
|
}>;
|
|
382
384
|
// New: Flattened list of business rule selections (for appendix)
|
|
383
385
|
business_rules_all?: Array<{
|