kyd-shared-badge 0.3.122 → 0.3.124
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
|
@@ -208,6 +208,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
208
208
|
setSelectedProviderIdAndCallback(null);
|
|
209
209
|
setLinkUrl('');
|
|
210
210
|
setShowDataHandling(false);
|
|
211
|
+
setPreviewProviderId(null);
|
|
212
|
+
setPreviewAction(null);
|
|
211
213
|
};
|
|
212
214
|
|
|
213
215
|
const cardVariants = {
|
|
@@ -239,7 +241,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
239
241
|
}, [githubConnectedAccount]);
|
|
240
242
|
|
|
241
243
|
// Progress UI is outsourced to ConnectProgress
|
|
242
|
-
|
|
244
|
+
|
|
243
245
|
return (
|
|
244
246
|
<div className="w-full grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_380px] lg:justify-center gap-6 items-start max-w-7xl">
|
|
245
247
|
<div className="w-full flex justify-end">
|
|
@@ -561,6 +563,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
561
563
|
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
562
564
|
<Button
|
|
563
565
|
onClick={() => setSelectedProviderIdAndCallback('githubapp')}
|
|
566
|
+
onMouseEnter={() => setPreview('githubapp', 'connect')}
|
|
567
|
+
onMouseLeave={clearPreview}
|
|
564
568
|
className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
|
|
565
569
|
>
|
|
566
570
|
<Lock className="size-3 sm:size-4" />
|
|
@@ -18,129 +18,162 @@ export type ConnectProgressProps = {
|
|
|
18
18
|
export function ConnectProgress(props: ConnectProgressProps) {
|
|
19
19
|
const { providers, connectedIds, className, layout = 'fixed-desktop', previewProviderId, previewAction, selectedProviderId } = props
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
21
|
+
const normalizeId = (s: string) => s.toLowerCase().replace(/[^a-z]/g, '')
|
|
22
|
+
const computeNormalizedSet = (ids: Set<string>) => {
|
|
23
|
+
const norm = new Set<string>()
|
|
24
|
+
ids.forEach(id => norm.add(normalizeId(id)))
|
|
25
|
+
return norm
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalizedIds = useMemo(() => computeNormalizedSet(connectedIds), [connectedIds])
|
|
29
|
+
const hasGithub = normalizedIds.has('github')
|
|
30
|
+
const hasLinkedIn = normalizedIds.has('linkedin')
|
|
23
31
|
const otherConnectedCount = useMemo(() => {
|
|
24
32
|
let count = 0
|
|
25
|
-
|
|
33
|
+
normalizedIds.forEach(id => { if (id !== 'github' && id !== 'githubapp' && id !== 'linkedin') count += 1 })
|
|
26
34
|
return count
|
|
27
|
-
}, [
|
|
35
|
+
}, [normalizedIds])
|
|
28
36
|
|
|
29
37
|
const computePercent = (ids: Set<string>) => {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
38
|
+
const norm = computeNormalizedSet(ids)
|
|
39
|
+
const gh = norm.has('github')
|
|
40
|
+
const gha = norm.has('githubapp')
|
|
41
|
+
const li = norm.has('linkedin')
|
|
32
42
|
let others = 0
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const othersWeight = Math.min(others, 2) * 10
|
|
40
|
-
return Math.max(0, Math.min(100, githubWeight + linkedinWeight + othersWeight))
|
|
43
|
+
norm.forEach(id => { if (id !== 'github' && id !== 'githubapp' && id !== 'linkedin') others += 1 })
|
|
44
|
+
const githubWeight = gh ? 60 : 0
|
|
45
|
+
const githubAppWeight = gha ? 20 : 0
|
|
46
|
+
const linkedinWeight = li ? 0 : 0 // LinkedIn is not included in the score
|
|
47
|
+
const othersWeight = others * 10
|
|
48
|
+
return Math.max(0, Math.min(100, githubWeight + githubAppWeight + linkedinWeight + othersWeight))
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
const progressPercent = useMemo(() => computePercent(connectedIds), [connectedIds])
|
|
44
52
|
|
|
45
53
|
const previewPercent = useMemo(() => {
|
|
46
|
-
const pid = (previewProviderId || '')
|
|
54
|
+
const pid = normalizeId(previewProviderId || '')
|
|
47
55
|
if (!pid || !previewAction) return null as number | null
|
|
48
|
-
|
|
49
|
-
const next = new Set<string>(Array.from(connectedIds))
|
|
56
|
+
const next = new Set<string>(Array.from(normalizedIds))
|
|
50
57
|
const has = next.has(pid)
|
|
51
58
|
if (previewAction === 'connect' && !has) next.add(pid)
|
|
52
59
|
if (previewAction === 'disconnect' && has) next.delete(pid)
|
|
53
60
|
const val = computePercent(next)
|
|
61
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
62
|
+
try {
|
|
63
|
+
// Debug details for preview behavior
|
|
64
|
+
const beforePercent = progressPercent
|
|
65
|
+
const beforeOthers = (() => {
|
|
66
|
+
let c = 0; normalizedIds.forEach(id => { if (id !== 'github' && id !== 'githubapp' && id !== 'linkedin') c += 1 })
|
|
67
|
+
return c
|
|
68
|
+
})()
|
|
69
|
+
let afterOthers = 0; next.forEach(id => { if (id !== 'github' && id !== 'githubapp' && id !== 'linkedin') afterOthers += 1 })
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.log('[ConnectProgress] preview', {
|
|
72
|
+
pid,
|
|
73
|
+
previewAction,
|
|
74
|
+
hasBefore: normalizedIds.has(pid),
|
|
75
|
+
beforePercent,
|
|
76
|
+
afterPercent: val,
|
|
77
|
+
beforeOthers,
|
|
78
|
+
afterOthers,
|
|
79
|
+
normalizedIds: Array.from(normalizedIds),
|
|
80
|
+
nextIds: Array.from(next),
|
|
81
|
+
})
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
54
84
|
return val === progressPercent ? null : val
|
|
55
|
-
}, [previewProviderId, previewAction,
|
|
85
|
+
}, [previewProviderId, previewAction, normalizedIds, progressPercent])
|
|
56
86
|
|
|
57
87
|
const progressCopy = useMemo(() => {
|
|
58
|
-
const score = progressPercent
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
headline: 'Get started — connect GitHub',
|
|
62
|
-
body: 'GitHub contributes ~65% of your profile strength. Add LinkedIn (+15%) and 1–2 other platforms (+10% each). 70%+ is strong; 100% is optional.'
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (hasGithub && score < 70) {
|
|
88
|
+
const score = previewPercent ?? progressPercent
|
|
89
|
+
|
|
90
|
+
if (score < 20) {
|
|
66
91
|
return {
|
|
67
|
-
headline: '
|
|
68
|
-
body: '
|
|
92
|
+
headline: 'Insufficient',
|
|
93
|
+
body: 'No meaningful data connections',
|
|
69
94
|
}
|
|
70
|
-
}
|
|
71
|
-
if (!hasGithub && score < 70) {
|
|
95
|
+
} else if (score < 40) {
|
|
72
96
|
return {
|
|
73
|
-
headline: '
|
|
74
|
-
body: '
|
|
97
|
+
headline: 'Sparse',
|
|
98
|
+
body: 'Limited verified sources',
|
|
75
99
|
}
|
|
76
|
-
}
|
|
77
|
-
if (score >= 70 && score < 90) {
|
|
100
|
+
} else if (score < 60) {
|
|
78
101
|
return {
|
|
79
|
-
headline: '
|
|
80
|
-
body: '
|
|
102
|
+
headline: 'Adequate',
|
|
103
|
+
body: 'Adequate → Minimum viable data coverage',
|
|
81
104
|
}
|
|
82
|
-
}
|
|
83
|
-
if (score >= 90) {
|
|
105
|
+
} else if (score < 80) {
|
|
84
106
|
return {
|
|
85
|
-
headline: '
|
|
86
|
-
body: '
|
|
107
|
+
headline: 'Robust',
|
|
108
|
+
body: 'Strong, multi-source data foundation',
|
|
87
109
|
}
|
|
88
|
-
}
|
|
89
|
-
if (score === 100) {
|
|
110
|
+
} else {
|
|
90
111
|
return {
|
|
91
|
-
headline: '
|
|
92
|
-
body: '
|
|
112
|
+
headline: 'Optimal',
|
|
113
|
+
body: 'Comprehensive, high-confidence dataset',
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
|
-
|
|
96
|
-
headline: 'Keep going',
|
|
97
|
-
body: 'Connect more platforms to strengthen your profile. 70%+ is strong; 100% is optional.'
|
|
98
|
-
}
|
|
99
|
-
}, [progressPercent, hasGithub])
|
|
116
|
+
}, [previewPercent, progressPercent]);
|
|
100
117
|
|
|
101
118
|
const selectedPulse = useMemo(() => {
|
|
102
|
-
const sid = (selectedProviderId || '')
|
|
119
|
+
const sid = normalizeId(selectedProviderId || '')
|
|
103
120
|
if (!sid || sid === 'githubapp') return { from: undefined as number | undefined, to: undefined as number | undefined }
|
|
104
121
|
// Do not show if already connected
|
|
105
|
-
if (
|
|
122
|
+
if (normalizedIds.has(sid)) return { from: undefined as number | undefined, to: undefined as number | undefined }
|
|
106
123
|
// Potential gain if they proceed with this provider
|
|
107
124
|
let gain = 0
|
|
108
125
|
if (sid === 'github') gain = 65
|
|
109
126
|
else if (sid === 'linkedin') gain = 15
|
|
110
127
|
else {
|
|
111
|
-
// Others contribute
|
|
128
|
+
// Others always contribute +10 each (no cap)
|
|
112
129
|
const othersAlready = otherConnectedCount
|
|
113
|
-
|
|
130
|
+
gain = 10
|
|
114
131
|
}
|
|
115
132
|
const to = Math.max(0, Math.min(100, progressPercent + gain))
|
|
133
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
134
|
+
try {
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log('[ConnectProgress] selectedPulse', {
|
|
137
|
+
sid,
|
|
138
|
+
progressPercent,
|
|
139
|
+
gain,
|
|
140
|
+
to,
|
|
141
|
+
othersAlready: otherConnectedCount,
|
|
142
|
+
})
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
116
145
|
return { from: progressPercent, to }
|
|
117
|
-
}, [selectedProviderId,
|
|
146
|
+
}, [selectedProviderId, normalizedIds, progressPercent, otherConnectedCount])
|
|
118
147
|
|
|
148
|
+
const isPreviewing = previewPercent !== null && previewPercent !== undefined
|
|
119
149
|
const valueToShow = previewPercent ?? progressPercent
|
|
150
|
+
const centerIndicator = useMemo(() => {
|
|
151
|
+
if (previewPercent == null) return null as 'up' | 'down' | null
|
|
152
|
+
return previewPercent > progressPercent ? 'up' : previewPercent < progressPercent ? 'down' : null
|
|
153
|
+
}, [previewPercent, progressPercent])
|
|
120
154
|
|
|
121
155
|
const content = (
|
|
122
156
|
<Card className={`border-[var(--icon-button-secondary)] ${className || ''}`} style={{ backgroundColor: 'var(--content-card-background)'}}>
|
|
123
|
-
<
|
|
124
|
-
<div className="flex items-
|
|
157
|
+
<div className="p-4">
|
|
158
|
+
<div className="flex items-start gap-3">
|
|
125
159
|
<ProgressCircle
|
|
126
160
|
value={valueToShow}
|
|
127
161
|
size={64}
|
|
128
162
|
thickness={6}
|
|
129
|
-
highlightFrom={selectedPulse.from}
|
|
130
|
-
highlightTo={selectedPulse.to}
|
|
131
|
-
highlightPulse={Boolean(selectedPulse.to)}
|
|
163
|
+
highlightFrom={isPreviewing ? undefined : selectedPulse.from}
|
|
164
|
+
highlightTo={isPreviewing ? undefined : selectedPulse.to}
|
|
165
|
+
highlightPulse={Boolean(selectedPulse.to) && !isPreviewing}
|
|
166
|
+
centerIndicator={centerIndicator}
|
|
167
|
+
indicatorPulse={Boolean(centerIndicator)}
|
|
132
168
|
/>
|
|
133
|
-
<div className="min-w-0 max-w-xs">
|
|
134
|
-
<div className="text-base font-semibold truncate
|
|
135
|
-
<div className="text-sm
|
|
169
|
+
<div className="min-w-0 max-w-xs flex flex-col gap-2 items-start justify-start">
|
|
170
|
+
<div className="text-base font-semibold truncate text-[var(--text-main)]">Profile Depth: {progressCopy.headline}</div>
|
|
171
|
+
<div className="text-sm text-[var(--text-secondary)]">
|
|
136
172
|
{progressCopy.body}
|
|
137
173
|
</div>
|
|
138
|
-
<div className="text-sm mt-1" style={{ color: 'var(--text-secondary)'}}>
|
|
139
|
-
Weighted score • {valueToShow}%
|
|
140
|
-
</div>
|
|
141
174
|
</div>
|
|
142
175
|
</div>
|
|
143
|
-
</
|
|
176
|
+
</div>
|
|
144
177
|
</Card>
|
|
145
178
|
)
|
|
146
179
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { scoreToCssVar, red as redHex, green as greenHex } from '../colors'
|
|
4
|
+
import { ArrowUpIcon } from 'lucide-react'
|
|
3
5
|
|
|
4
6
|
export type ProgressCircleProps = {
|
|
5
7
|
value: number
|
|
@@ -12,10 +14,13 @@ export type ProgressCircleProps = {
|
|
|
12
14
|
highlightFrom?: number
|
|
13
15
|
highlightTo?: number
|
|
14
16
|
highlightPulse?: boolean
|
|
17
|
+
// Optional center indicator arrow: 'up' (green) or 'down' (red)
|
|
18
|
+
centerIndicator?: 'up' | 'down' | null
|
|
19
|
+
indicatorPulse?: boolean
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export function ProgressCircle(props: ProgressCircleProps) {
|
|
18
|
-
const { value, size = 72, thickness = 6, className, showLabel = true, label, highlightFrom, highlightTo, highlightPulse } = props
|
|
23
|
+
const { value, size = 72, thickness = 6, className, showLabel = true, label, highlightFrom, highlightTo, highlightPulse, centerIndicator, indicatorPulse } = props
|
|
19
24
|
|
|
20
25
|
const rawId = React.useId()
|
|
21
26
|
const gradientId = React.useMemo(() => `kyd-pc-${String(rawId).replace(/:/g, '')}` , [rawId])
|
|
@@ -31,6 +36,12 @@ export function ProgressCircle(props: ProgressCircleProps) {
|
|
|
31
36
|
const highlightLength = hasHighlight ? circumference * ((toClamped - fromClamped) / 100) : 0
|
|
32
37
|
const highlightOffset = hasHighlight ? circumference * (1 - toClamped / 100) : 0
|
|
33
38
|
|
|
39
|
+
const strokeColor = React.useMemo(() => scoreToCssVar(clamped), [clamped])
|
|
40
|
+
const indicatorColor = React.useMemo(() => {
|
|
41
|
+
if (!centerIndicator) return undefined
|
|
42
|
+
return centerIndicator === 'up' ? `var(--status-positive, ${greenHex})` : `var(--status-negative, ${redHex})`
|
|
43
|
+
}, [centerIndicator])
|
|
44
|
+
|
|
34
45
|
return (
|
|
35
46
|
<div className={`relative inline-flex items-center justify-center ${className || ''}`} style={{ width: size, height: size }}>
|
|
36
47
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="block">
|
|
@@ -54,12 +65,12 @@ export function ProgressCircle(props: ProgressCircleProps) {
|
|
|
54
65
|
cy={size / 2}
|
|
55
66
|
r={radius}
|
|
56
67
|
fill="none"
|
|
57
|
-
stroke={
|
|
68
|
+
stroke={strokeColor}
|
|
58
69
|
strokeWidth={thickness}
|
|
59
70
|
strokeLinecap="round"
|
|
60
71
|
strokeDasharray={circumference}
|
|
61
72
|
strokeDashoffset={dashOffset}
|
|
62
|
-
style={{ transition: 'stroke-dashoffset 800ms ease' }}
|
|
73
|
+
style={{ transition: 'stroke-dashoffset 800ms ease, stroke 400ms ease, stroke-opacity 400ms ease' }}
|
|
63
74
|
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
|
64
75
|
/>
|
|
65
76
|
{hasHighlight ? (
|
|
@@ -84,18 +95,16 @@ export function ProgressCircle(props: ProgressCircleProps) {
|
|
|
84
95
|
cy={size / 2}
|
|
85
96
|
r={radius + thickness / 2}
|
|
86
97
|
fill="none"
|
|
87
|
-
stroke=
|
|
98
|
+
stroke={strokeColor}
|
|
88
99
|
strokeOpacity={0.15}
|
|
89
100
|
strokeWidth={2}
|
|
90
|
-
className="animate-pulse"
|
|
101
|
+
// className="animate-pulse"
|
|
102
|
+
// style={{ transition: 'stroke 200ms ease, stroke-opacity 200ms ease' }}
|
|
91
103
|
/>
|
|
92
104
|
</svg>
|
|
93
|
-
{
|
|
94
|
-
<div
|
|
95
|
-
className="
|
|
96
|
-
style={{ color: 'var(--text-main)' }}
|
|
97
|
-
>
|
|
98
|
-
{label ? label : `${Math.round(clamped)}%`}
|
|
105
|
+
{centerIndicator ? (
|
|
106
|
+
<div className={`absolute inset-0 flex items-center justify-center`} aria-hidden>
|
|
107
|
+
<ArrowUpIcon className="w-4 h-4 text-[var(--icon-accent)]" />
|
|
99
108
|
</div>
|
|
100
109
|
) : null}
|
|
101
110
|
</div>
|