kyd-shared-badge 0.3.70 → 0.3.72

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.70",
3
+ "version": "0.3.72",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -22,12 +22,14 @@
22
22
  "@chatscope/chat-ui-kit-react": "^2.1.1",
23
23
  "@chatscope/chat-ui-kit-styles": "^1.4.0",
24
24
  "ai": "5.0.47",
25
+ "framer-motion": "^12.23.24",
25
26
  "i18n-iso-countries": "^7.14.0",
26
27
  "lucide-react": "^0.545.0",
27
28
  "next-auth": "^4.24.11",
28
29
  "react-hot-toast": "^2.6.0",
29
30
  "react-icons": "^5.5.0",
30
31
  "recharts": "^2.15.4",
32
+ "tailwind-merge": "^3.3.1",
31
33
  "ulid": "^3.0.1"
32
34
  },
33
35
  "publishConfig": {
@@ -3,8 +3,10 @@ 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 } from 'lucide-react';
7
- import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter } from '../ui';
6
+ import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink } from 'lucide-react';
7
+ import { AnimatePresence, motion } from 'framer-motion';
8
+ import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
9
+ import Link from 'next/link';
8
10
 
9
11
  function byPriority(a: string, b: string) {
10
12
  const pr = (id: string) => (id === 'github' ? 0 : id === 'linkedin' ? 1 : 2);
@@ -28,6 +30,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
28
30
  onDisconnected,
29
31
  onError,
30
32
  className,
33
+ oauthClientIds,
34
+ providerPickCallback,
35
+ handleBackButton,
36
+ headerTitle,
37
+ headerDescription,
31
38
  } = props;
32
39
 
33
40
  const router = useRouter();
@@ -89,6 +96,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
89
96
  }
90
97
  };
91
98
 
99
+ const setSelectedProviderIdAndCallback = (providerId: string | null) => {
100
+ setSelectedProviderId(providerId);
101
+ providerPickCallback(providerId);
102
+ };
103
+
92
104
  const onSubmitLink = async (providerId: string) => {
93
105
  const provider = providers.find(p => p.id.toLowerCase() === providerId.toLowerCase());
94
106
  if (!provider || !provider.endpoint) return;
@@ -106,7 +118,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
106
118
  });
107
119
  const data = await res.json().catch(() => ({}));
108
120
  if (!res.ok) throw new Error(data?.error || `Failed to sync ${provider.name} profile.`);
109
- setSelectedProviderId(null);
121
+ setSelectedProviderIdAndCallback(null);
122
+
110
123
  setLinkUrl('');
111
124
  if (onConnected) onConnected(providerId);
112
125
  } catch (e) {
@@ -124,136 +137,247 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
124
137
  // Lazy import builder to avoid circular deps
125
138
  // eslint-disable-next-line @typescript-eslint/no-var-requires
126
139
  const { buildOAuthUrl } = require('./oauth');
127
- const url = buildOAuthUrl({ providerId: id, redirectUri, state });
140
+ const url = buildOAuthUrl({ providerId: id, redirectUri, state, clientIds: oauthClientIds });
128
141
  if (url) router.push(url);
129
142
  };
130
143
 
131
- return (
132
- <div className={className}>
133
- {(() => {
134
- const oauthList = list.filter(p => p.connectionType === 'oauth');
135
- const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
144
+ const selectedProvider = useMemo(
145
+ () => (selectedProviderId ? providers.find(p => p.id.toLowerCase() === selectedProviderId.toLowerCase()) : null),
146
+ [selectedProviderId, providers]
147
+ );
136
148
 
137
- const Row = (provider: typeof list[number]) => {
138
- const providerId = provider.id;
139
- const isConnected = connectedIds.has(providerId.toLowerCase());
140
- const needsReconnect = reconnectIds.has(providerId.toLowerCase());
141
- const isUrl = (provider.connectionType || 'url') === 'url' || provider.connectionType === 'link';
142
- const isOauth = provider.connectionType === 'oauth';
143
- return (
144
- <div key={providerId} className="group flex items-center justify-between p-2 rounded-lg transition-colors hover:bg-[var(--icon-button-secondary)]/10 transform transition-transform hover:-translate-y-px">
145
- <div className="flex items-center gap-3">
146
- <ProviderIcon name={providerId} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
147
- <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
148
- {provider.beta ? (
149
- <span
150
- className="ml-2 inline-block rounded-full px-2 py-0.5 text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200"
151
- style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
152
- >
153
- Beta
154
- </span>
155
- ) : null}
149
+ const handleConnectBack = () => {
150
+ setSelectedProviderIdAndCallback(null);
151
+ setLinkUrl('');
152
+ };
153
+
154
+ const cardVariants = {
155
+ initial: { opacity: 0, y: 20 },
156
+ animate: { opacity: 1, y: 0 },
157
+ exit: { opacity: 0, y: -20 },
158
+ };
159
+
160
+ return (
161
+ <>
162
+ {selectedProvider ? (
163
+ <AnimatePresence>
164
+ <motion.div
165
+ key="connect-card"
166
+ className="rounded-xl border max-w-xl w-full"
167
+ style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
168
+ initial="initial" animate="animate" exit="exit" variants={cardVariants}
169
+ >
170
+ <div className="sm:p-6 p-4">
171
+ <button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
172
+ <ArrowLeft className="w-4 h-4" />
173
+ Back
174
+ </button>
175
+ <div className="text-center">
176
+ <div className="flex justify-center mb-4">
177
+ <ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
178
+ </div>
179
+ <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
180
+ {selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
181
+ ? `Use Public ${selectedProvider.name} Profile`
182
+ : `Connect ${selectedProvider.name}`}
183
+ </h3>
184
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
185
+ {(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
186
+ ? (selectedProvider.placeholder || 'Enter your public profile URL.')
187
+ : `Authorize with ${selectedProvider.name} to connect your account.`}
188
+ </p>
156
189
  </div>
157
190
 
158
- {isConnected ? (
159
- <div className="relative flex items-center">
160
- <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
161
- {/* CheckCircle icon (inline SVG) */}
162
- <CheckCircle className="size-3 sm:size-4" />
163
- <span className="text-sm font-medium">Connected</span>
191
+ {(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
192
+ <motion.form
193
+ onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
194
+ className="mt-6 space-y-4"
195
+ initial="initial" animate="animate" exit="exit" variants={cardVariants}
196
+ >
197
+ {selectedProvider.id === 'linkedin' && (
198
+ <p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
199
+ <Link
200
+ href="https://www.linkedin.com/public-profile/settings"
201
+ target="_blank"
202
+ rel="noopener noreferrer"
203
+ className="underline"
204
+ style={{ color: 'var(--icon-accent)' }}
205
+ >
206
+ LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
207
+ </Link>
208
+ . This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
209
+ </p>
210
+ )}
211
+ <div className="relative">
212
+ <LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
213
+ <Input
214
+ type="url"
215
+ value={linkUrl}
216
+ onChange={(e) => setLinkUrl(e.target.value)}
217
+ placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
218
+ required
219
+ className="w-full border bg-transparent p-2 pl-9"
220
+ style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
221
+ onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
222
+ onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
223
+ />
164
224
  </div>
165
- <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
166
- <Button
167
- onClick={() => onDisconnect(providerId)}
168
- className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
169
- >
170
- {/* Unlink icon (inline SVG) */}
171
- <Unlink className="size-3 sm:size-4" />
172
- <span>Disconnect</span>
225
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
226
+ <Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
227
+ {isSubmitting ? (
228
+ <div className="flex items-center justify-center">
229
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
230
+ Connecting...
231
+ </div>
232
+ ) : (
233
+ 'Connect'
234
+ )}
173
235
  </Button>
174
- </div>
175
- </div>
236
+ </motion.div>
237
+ </motion.form>
176
238
  ) : (
177
- <div className="flex items-center gap-2">
178
- {selectedProviderId === providerId && isUrl ? (
179
- <form onSubmit={(e) => { e.preventDefault(); onSubmitLink(providerId); }} className="flex items-center gap-2">
180
- <div className="relative">
181
- <Input
182
- type="url"
183
- value={linkUrl}
184
- onChange={(e) => setLinkUrl(e.target.value)}
185
- placeholder={provider.placeholder || 'https://example.com/your-profile'}
186
- required
187
- className="w-72 border bg-transparent p-2 text-sm rounded"
188
- style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
189
- onPaste={providerId === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
190
- onBlur={providerId === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
191
- />
192
- </div>
193
- <Button type="submit" disabled={isSubmitting} className="px-3 py-2 rounded bg-[var(--icon-accent)] text-white">
194
- {isSubmitting ? 'Connecting…' : 'Connect'}
195
- </Button>
196
- </form>
197
- ) : (
198
- <>
199
- <Button
200
- onClick={() => (isOauth ? onOAuth(providerId) : setSelectedProviderId(providerId))}
201
- 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"
202
- >
203
- {/* OAuth/URL leading icon */}
204
- {isOauth ? (
205
- <Link2 className="size-3 sm:size-4" />
206
- ) : (
207
- <LinkIcon className="size-3 sm:size-4" />
208
- )}
209
- <span className="sm:text-base text-sm">Connect</span>
210
- </Button>
211
- {needsReconnect && (
212
- <Button
213
- onClick={() => onDisconnect(providerId)}
214
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
215
- style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
216
- >
217
- {/* Unlink icon (inline SVG) */}
218
- <Unlink className="size-3 sm:size-4" />
219
- <span>Remove</span>
220
- </Button>
221
- )}
222
- </>
223
- )}
239
+ <div className="mt-6">
240
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
241
+ <Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
242
+ <ExternalLink className="w-4 h-4 mr-2" />
243
+ Connect with {selectedProvider.name}
244
+ </Button>
245
+ </motion.div>
224
246
  </div>
225
247
  )}
226
248
  </div>
227
- );
228
- };
249
+ </motion.div>
250
+ </AnimatePresence>
251
+ ) : (
252
+ <Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
253
+ <AnimatePresence mode="wait">
254
+ <motion.div key="platform-list-shared" variants={cardVariants} initial="initial" animate="animate" exit="exit" transition={{ duration: 0.3 }}>
255
+ <CardHeader className="pb-4">
256
+ {handleBackButton && (
257
+ <button onClick={() => handleBackButton()} className="flex items-center gap-2 text-sm sm:mb-1 mb-4 text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors">
258
+ <ArrowLeft className="w-4 h-4" /> Back
259
+ </button>
260
+ )}
261
+ <CardTitle className="sm:text-xl text-base font-semibold text-[var(--text-main)] mb-2">{headerTitle}</CardTitle>
262
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed">{headerDescription}</p>
263
+ </CardHeader>
264
+ <CardContent className="space-y-2">
265
+ {(() => {
266
+ const oauthList = list.filter(p => p.connectionType === 'oauth');
267
+ const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
229
268
 
230
- return (
231
- <>
232
- {oauthList.length ? (
233
- <>
234
- <div className="flex items-center my-2">
235
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
236
- <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
237
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
238
- </div>
239
- {oauthList.map(p => Row(p))}
240
- </>
241
- ) : null}
269
+ const Row = (provider: typeof list[number]) => {
270
+ const providerId = provider.id;
271
+ const isConnected = connectedIds.has(providerId.toLowerCase());
272
+ const needsReconnect = reconnectIds.has(providerId.toLowerCase());
273
+ const isOauth = provider.connectionType === 'oauth'
274
+ const connectedUrl = connected.find(c => c.id.toLowerCase() === providerId.toLowerCase())?.url;
275
+ const betaFlag = () => (
276
+ <span
277
+ className="ml-2 inline-block rounded-full px-2 py-0.5 text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200"
278
+ style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
279
+ >
280
+ Beta
281
+ </span>
282
+ );
242
283
 
243
- {urlList.length ? (
244
- <>
245
- <div className="flex items-center my-2">
246
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
247
- <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
248
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
249
- </div>
250
- {urlList.map(p => Row(p))}
251
- </>
252
- ) : null}
253
- </>
254
- );
255
- })()}
256
- </div>
284
+ return (
285
+ <div key={providerId} className="group flex items-center justify-between p-2 rounded-lg transition-colors hover:bg-[var(--icon-button-secondary)]/10 transform transition-transform hover:-translate-y-px">
286
+ <div className="flex items-center gap-3">
287
+
288
+ {isConnected && connectedUrl ? (
289
+ <Link href={connectedUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 hover:underline">
290
+ <ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
291
+ <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
292
+ </Link>
293
+ ) : (
294
+ <div className="flex items-center gap-3">
295
+ <ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
296
+ <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
297
+ </div>
298
+ )}
299
+ {provider.beta ? betaFlag() : null}
300
+ </div>
301
+
302
+ {isConnected ? (
303
+ <div className="relative flex items-center">
304
+ <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
305
+ <CheckCircle className="size-3 sm:size-4" />
306
+ <span className="text-sm font-medium">Connected</span>
307
+ </div>
308
+ <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
309
+ <button
310
+ onClick={() => onDisconnect(providerId)}
311
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
312
+ >
313
+ <Unlink className="size-3 sm:size-4" />
314
+ <span>Disconnect</span>
315
+ </button>
316
+ </div>
317
+ </div>
318
+ ) : (
319
+ <div className="flex items-center gap-2">
320
+ <>
321
+ <Button
322
+ onClick={() => setSelectedProviderIdAndCallback(providerId)}
323
+ 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"
324
+ >
325
+ {isOauth ? (
326
+ <Link2 className="size-3 sm:size-4" />
327
+ ) : (
328
+ <LinkIcon className="size-3 sm:size-4" />
329
+ )}
330
+ <span className="sm:text-base text-sm">Connect</span>
331
+ </Button>
332
+ {needsReconnect && (
333
+ <Button
334
+ onClick={() => onDisconnect(providerId)}
335
+ className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
336
+ style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
337
+ >
338
+ <Unlink className="size-3 sm:size-4" />
339
+ <span>Remove</span>
340
+ </Button>
341
+ )}
342
+ </>
343
+ </div>
344
+ )}
345
+ </div>
346
+ );
347
+ };
348
+
349
+ return (
350
+ <>
351
+ {oauthList.length ? (
352
+ <>
353
+ <div className="flex items-center my-2">
354
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
355
+ <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
356
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
357
+ </div>
358
+ {oauthList.map(p => Row(p))}
359
+ </>
360
+ ) : null}
361
+
362
+ {urlList.length ? (
363
+ <>
364
+ <div className="flex items-center my-2">
365
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
366
+ <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
367
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
368
+ </div>
369
+ {urlList.map(p => Row(p))}
370
+ </>
371
+ ) : null}
372
+ </>
373
+ );
374
+ })()}
375
+ </CardContent>
376
+ </motion.div>
377
+ </AnimatePresence>
378
+ </Card>
379
+ )}
380
+ </>
257
381
  );
258
382
  }
259
383
 
@@ -1,15 +1,16 @@
1
1
  type KnownProvider = 'github' | 'gitlab' | 'stackoverflow';
2
2
 
3
3
  const env = (key: string): string | undefined => {
4
- // Access process.env directly; Next.js will inline NEXT_PUBLIC_* at build time
5
- try { return (process.env as any)[key]; } catch { return undefined; }
4
+ // Use globalThis to avoid hard dependency on Node 'process' types/environ
5
+ try {
6
+ const g = globalThis as unknown as { process?: { env?: Record<string, string | undefined> } };
7
+ return g?.process?.env?.[key];
8
+ } catch {
9
+ return undefined;
10
+ }
6
11
  };
7
12
 
8
- const CLIENT_IDS: Record<KnownProvider, string | undefined> = {
9
- github: env('NEXT_PUBLIC_GITHUB_CLIENT_ID'),
10
- gitlab: env('NEXT_PUBLIC_GITLAB_CLIENT_ID'),
11
- stackoverflow: env('NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID'),
12
- };
13
+ export type OAuthClientIds = Partial<Record<KnownProvider, string>>;
13
14
 
14
15
  const DEFAULT_SCOPES: Record<KnownProvider, string> = {
15
16
  github: 'read:user,repo',
@@ -22,9 +23,13 @@ export function buildOAuthUrl(params: {
22
23
  redirectUri: string;
23
24
  state?: string;
24
25
  scopeOverride?: string;
26
+ clientIds?: OAuthClientIds;
25
27
  }): string {
26
28
  const id = (params.providerId || '').toLowerCase() as KnownProvider;
27
- const clientId = CLIENT_IDS[id as KnownProvider];
29
+ const clientId = (params.clientIds && params.clientIds[id]) || env(
30
+ id === 'github' ? 'NEXT_PUBLIC_GITHUB_CLIENT_ID' : id === 'gitlab' ? 'NEXT_PUBLIC_GITLAB_CLIENT_ID' : 'NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID'
31
+ );
32
+ // clientId should be inlined at build-time for NEXT_PUBLIC_*
28
33
  const scope = params.scopeOverride ?? DEFAULT_SCOPES[id as KnownProvider] ?? '';
29
34
  const redirect = encodeURIComponent(params.redirectUri);
30
35
  const state = params.state ? `&state=${encodeURIComponent(params.state)}` : '';
@@ -27,10 +27,15 @@ export interface ConnectAccountsProps {
27
27
  buildOAuthState?: () => Record<string, string | number | boolean> | string | undefined;
28
28
  stateFormat?: StateFormat;
29
29
  redirectUriOverrides?: RedirectUriOverrides;
30
+ oauthClientIds?: Partial<Record<'github' | 'gitlab' | 'stackoverflow', string>>;
30
31
  onConnected?: (providerId: string) => void;
31
32
  onDisconnected?: (providerId: string) => void;
32
33
  onError?: (message: string) => void;
33
34
  className?: string;
35
+ providerPickCallback: (providerId: string | null) => void;
36
+ handleBackButton?: () => void;
37
+ headerTitle: string;
38
+ headerDescription: string;
34
39
  }
35
40
 
36
41
 
package/src/ui/card.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from "react"
2
2
 
3
- import { cn } from "@/lib/utils"
3
+ import { cn } from "../utils"
4
4
 
5
5
  const Card = React.forwardRef<
6
6
  HTMLDivElement,
@@ -0,0 +1,2 @@
1
+ export * from './date';
2
+ export * from './utils';
@@ -0,0 +1,9 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export const ROLE_NAME_MAX_LENGTH = 500;
9
+ export const ROLE_DESCRIPTION_MAX_LENGTH = 5500;