kyd-shared-badge 0.3.122 → 0.3.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.122",
3
+ "version": "0.3.123",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -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,156 @@ 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 hasGithub = connectedIds.has('github')
22
- const hasLinkedIn = connectedIds.has('linkedin')
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
- connectedIds.forEach(id => { if (id !== 'github' && id !== 'linkedin') count += 1 })
33
+ normalizedIds.forEach(id => { if (id !== 'github' && id !== 'githubapp' && id !== 'linkedin') count += 1 })
26
34
  return count
27
- }, [connectedIds])
35
+ }, [normalizedIds])
28
36
 
29
37
  const computePercent = (ids: Set<string>) => {
30
- const gh = ids.has('github')
31
- const li = ids.has('linkedin')
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
- ids.forEach(id => {
34
- const lid = id.toLowerCase()
35
- if (lid !== 'github' && lid !== 'linkedin') others += 1
36
- })
37
- const githubWeight = gh ? 65 : 0
38
- const linkedinWeight = li ? 15 : 0
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 || '').toLowerCase()
54
+ const pid = normalizeId(previewProviderId || '')
47
55
  if (!pid || !previewAction) return null as number | null
48
- if (pid === 'githubapp') return null
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, connectedIds, progressPercent])
85
+ }, [previewProviderId, previewAction, normalizedIds, progressPercent])
56
86
 
57
87
  const progressCopy = useMemo(() => {
58
- const score = progressPercent
59
- if (score === 0) {
60
- return {
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: 'Good base — add more signals',
68
- body: 'Add LinkedIn (+15%) and at least one other platform (+10%) to reach 70%+, which is considered strong. 100% is not mandatory.'
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: 'Add GitHub for a big boost',
74
- body: 'Current signals are light. Connecting GitHub adds ~65% weight and significantly improves your profile.'
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: 'Strong — recommended coverage achieved',
80
- body: '70%+ indicates a strong profile. Adding more platforms can further improve verification. 100% is optional but even better.'
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: 'Excellent — high confidence',
86
- body: 'You are near complete coverage. 100% is not required, but additional platforms can further increase confidence.'
107
+ headline: 'Robust',
108
+ body: 'Strong, multi-source data foundation',
87
109
  }
88
- }
89
- if (score === 100) {
110
+ } else {
90
111
  return {
91
- headline: 'Complete — high confidence',
92
- body: 'You\'ve achieved the recommended coverage for a very strong profile! Connecting even more accounts can further strengthen your verification.'
112
+ headline: 'Optimal',
113
+ body: 'Comprehensive, high-confidence dataset',
93
114
  }
94
115
  }
95
- return {
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 || '').toLowerCase()
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 (connectedIds.has(sid)) return { from: undefined as number | undefined, to: undefined as number | undefined }
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 up to 2*10 capped
128
+ // Others always contribute +10 each (no cap)
112
129
  const othersAlready = otherConnectedCount
113
- if (othersAlready < 2) gain = 10
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, connectedIds, progressPercent, otherConnectedCount])
146
+ }, [selectedProviderId, normalizedIds, progressPercent, otherConnectedCount])
118
147
 
148
+ const isPreviewing = previewPercent !== null && previewPercent !== undefined
119
149
  const valueToShow = previewPercent ?? progressPercent
120
150
 
121
151
  const content = (
122
152
  <Card className={`border-[var(--icon-button-secondary)] ${className || ''}`} style={{ backgroundColor: 'var(--content-card-background)'}}>
123
- <CardContent className="py-3 px-3">
124
- <div className="flex items-center gap-3">
153
+ <div className="p-4">
154
+ <div className="flex items-start gap-3">
125
155
  <ProgressCircle
126
156
  value={valueToShow}
127
157
  size={64}
128
158
  thickness={6}
129
- highlightFrom={selectedPulse.from}
130
- highlightTo={selectedPulse.to}
131
- highlightPulse={Boolean(selectedPulse.to)}
159
+ highlightFrom={isPreviewing ? undefined : selectedPulse.from}
160
+ highlightTo={isPreviewing ? undefined : selectedPulse.to}
161
+ highlightPulse={Boolean(selectedPulse.to) && !isPreviewing}
132
162
  />
133
- <div className="min-w-0 max-w-xs">
134
- <div className="text-base font-semibold truncate" style={{ color: 'var(--text-main)'}}>{progressCopy.headline}</div>
135
- <div className="text-sm mt-0.5" style={{ color: 'var(--text-secondary)'}}>
163
+ <div className="min-w-0 max-w-xs flex flex-col gap-2 items-start justify-start">
164
+ <div className="text-base font-semibold truncate text-[var(--text-main)]">{progressCopy.headline}</div>
165
+ <div className="text-sm text-[var(--text-secondary)]">
136
166
  {progressCopy.body}
137
167
  </div>
138
- <div className="text-sm mt-1" style={{ color: 'var(--text-secondary)'}}>
139
- Weighted score • {valueToShow}%
140
- </div>
141
168
  </div>
142
169
  </div>
143
- </CardContent>
170
+ </div>
144
171
  </Card>
145
172
  )
146
173
 
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
  import React from 'react'
3
+ import { scoreToCssVar } from '../colors'
3
4
 
4
5
  export type ProgressCircleProps = {
5
6
  value: number
@@ -31,6 +32,8 @@ export function ProgressCircle(props: ProgressCircleProps) {
31
32
  const highlightLength = hasHighlight ? circumference * ((toClamped - fromClamped) / 100) : 0
32
33
  const highlightOffset = hasHighlight ? circumference * (1 - toClamped / 100) : 0
33
34
 
35
+ const strokeColor = React.useMemo(() => scoreToCssVar(clamped), [clamped])
36
+
34
37
  return (
35
38
  <div className={`relative inline-flex items-center justify-center ${className || ''}`} style={{ width: size, height: size }}>
36
39
  <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="block">
@@ -54,12 +57,12 @@ export function ProgressCircle(props: ProgressCircleProps) {
54
57
  cy={size / 2}
55
58
  r={radius}
56
59
  fill="none"
57
- stroke={`url(#${gradientId})`}
60
+ stroke={strokeColor}
58
61
  strokeWidth={thickness}
59
62
  strokeLinecap="round"
60
63
  strokeDasharray={circumference}
61
64
  strokeDashoffset={dashOffset}
62
- style={{ transition: 'stroke-dashoffset 800ms ease' }}
65
+ style={{ transition: 'stroke-dashoffset 800ms ease, stroke 400ms ease, stroke-opacity 400ms ease' }}
63
66
  transform={`rotate(-90 ${size / 2} ${size / 2})`}
64
67
  />
65
68
  {hasHighlight ? (
@@ -84,20 +87,14 @@ export function ProgressCircle(props: ProgressCircleProps) {
84
87
  cy={size / 2}
85
88
  r={radius + thickness / 2}
86
89
  fill="none"
87
- stroke="var(--icon-accent)"
90
+ stroke={strokeColor}
88
91
  strokeOpacity={0.15}
89
92
  strokeWidth={2}
90
93
  className="animate-pulse"
94
+ style={{ transition: 'stroke 400ms ease, stroke-opacity 400ms ease' }}
91
95
  />
92
96
  </svg>
93
- {showLabel ? (
94
- <div
95
- className="absolute inset-0 flex items-center justify-center text-[10px] sm:text-xs font-medium"
96
- style={{ color: 'var(--text-main)' }}
97
- >
98
- {label ? label : `${Math.round(clamped)}%`}
99
- </div>
100
- ) : null}
97
+
101
98
  </div>
102
99
  )
103
100
  }