@sublimee/auth-ui 0.1.1 → 0.1.13
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/AI_INDEX.md +64 -0
- package/README.md +80 -120
- package/dist/animations.css +52 -0
- package/dist/index.d.mts +59 -42
- package/dist/index.d.ts +59 -42
- package/dist/index.js +443 -56
- package/dist/index.mjs +441 -54
- package/package.json +9 -6
- package/src/animations.css +52 -0
- package/src/base-button.stories.tsx +96 -0
- package/src/base-button.tsx +124 -0
- package/src/button-loading-indicator.tsx +33 -0
- package/src/icons.tsx +17 -24
- package/src/index.ts +25 -19
- package/src/oauth-button.stories.tsx +72 -333
- package/src/oauth-button.tsx +34 -79
- package/src/runtime-styles.ts +225 -0
- package/src/types.ts +66 -10
- package/src/use-button-animation.ts +114 -0
|
@@ -1,385 +1,124 @@
|
|
|
1
|
-
import type { Story } from
|
|
2
|
-
import { useState, useCallback, useRef } from "react";
|
|
3
|
-
import { OAuthButton } from "./oauth-button";
|
|
4
|
-
import type { OAuthProvider } from "./types";
|
|
1
|
+
import type { Story } from '@ladle/react';
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* - Presses immediately on mousedown (scale down + shadow-inner)
|
|
9
|
-
* - Stays pressed while holding
|
|
10
|
-
* - Plays release animation on mouseup (always completes fully)
|
|
11
|
-
*/
|
|
12
|
-
function useRealPress() {
|
|
13
|
-
const [phase, setPhase] = useState<"idle" | "pressed" | "releasing">("idle");
|
|
14
|
-
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
15
|
-
|
|
16
|
-
const onMouseDown = useCallback(() => {
|
|
17
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
18
|
-
setPhase("pressed");
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
const onMouseUp = useCallback(() => {
|
|
22
|
-
if (phase !== "pressed") return;
|
|
23
|
-
setPhase("releasing");
|
|
24
|
-
timeoutRef.current = setTimeout(() => setPhase("idle"), 200);
|
|
25
|
-
}, [phase]);
|
|
3
|
+
import { OAuthButton } from './oauth-button';
|
|
4
|
+
import type { OAuthProvider, ButtonVariant } from './types';
|
|
26
5
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const className =
|
|
32
|
-
phase === "pressed" ? "scale-[0.96] shadow-inner" :
|
|
33
|
-
phase === "releasing" ? "animate-button-release" : "";
|
|
34
|
-
|
|
35
|
-
return { className, onMouseDown, onMouseUp, onMouseLeave };
|
|
36
|
-
}
|
|
6
|
+
export default {
|
|
7
|
+
title: 'B Auth Button',
|
|
8
|
+
};
|
|
37
9
|
|
|
38
10
|
/**
|
|
39
11
|
* OAuthButton stories
|
|
40
12
|
*
|
|
41
|
-
*
|
|
13
|
+
* Semantic OAuth buttons with curated motion and built-in loading behavior.
|
|
14
|
+
* These components adapt to your theme via CSS custom properties.
|
|
42
15
|
*/
|
|
43
16
|
|
|
44
17
|
interface OAuthButtonStoryProps {
|
|
45
18
|
provider: OAuthProvider;
|
|
19
|
+
variant: ButtonVariant;
|
|
46
20
|
disabled: boolean;
|
|
47
21
|
loading: boolean;
|
|
48
22
|
label: string;
|
|
49
23
|
}
|
|
50
24
|
|
|
51
|
-
const baseStyles =
|
|
52
|
-
"flex items-center justify-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg font-medium border border-gray-300 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:shadow transition-all duration-75 cursor-pointer";
|
|
53
|
-
|
|
54
|
-
const darkStyles =
|
|
55
|
-
"dark:bg-gray-900 dark:text-white dark:border-gray-700 dark:hover:bg-gray-800";
|
|
56
|
-
|
|
57
25
|
export const Default: Story<OAuthButtonStoryProps> = ({
|
|
58
26
|
provider,
|
|
27
|
+
variant,
|
|
59
28
|
disabled,
|
|
60
29
|
loading,
|
|
61
30
|
label,
|
|
62
|
-
}) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return (
|
|
31
|
+
}) => (
|
|
32
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
66
33
|
<OAuthButton
|
|
67
34
|
provider={provider}
|
|
35
|
+
variant={variant}
|
|
68
36
|
disabled={disabled}
|
|
69
37
|
loading={loading}
|
|
70
|
-
onMouseDown={press.onMouseDown}
|
|
71
|
-
onMouseUp={press.onMouseUp}
|
|
72
|
-
onMouseLeave={press.onMouseLeave}
|
|
73
|
-
className={`${baseStyles} ${darkStyles} ${press.className}`}
|
|
74
38
|
>
|
|
75
39
|
{label || undefined}
|
|
76
40
|
</OAuthButton>
|
|
77
|
-
|
|
78
|
-
|
|
41
|
+
|
|
42
|
+
<OAuthButton
|
|
43
|
+
provider={provider}
|
|
44
|
+
variant="secondary"
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
loading={loading}
|
|
47
|
+
>
|
|
48
|
+
{null}
|
|
49
|
+
</OAuthButton>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
79
52
|
|
|
80
53
|
Default.args = {
|
|
81
|
-
provider:
|
|
54
|
+
provider: 'google',
|
|
55
|
+
variant: 'secondary',
|
|
82
56
|
disabled: false,
|
|
83
57
|
loading: false,
|
|
84
|
-
label:
|
|
58
|
+
label: '',
|
|
85
59
|
};
|
|
86
60
|
|
|
87
61
|
Default.argTypes = {
|
|
88
62
|
provider: {
|
|
89
|
-
control: { type:
|
|
90
|
-
options: [
|
|
63
|
+
control: { type: 'select' },
|
|
64
|
+
options: ['google', 'discord'],
|
|
65
|
+
},
|
|
66
|
+
variant: {
|
|
67
|
+
control: { type: 'select' },
|
|
68
|
+
options: ['primary', 'secondary'],
|
|
91
69
|
},
|
|
92
70
|
label: {
|
|
93
|
-
control: { type:
|
|
71
|
+
control: { type: 'text' },
|
|
94
72
|
},
|
|
95
73
|
};
|
|
96
74
|
|
|
97
|
-
export const
|
|
98
|
-
<div className="flex flex-col gap-4">
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
</
|
|
106
|
-
<OAuthButton
|
|
107
|
-
provider="discord"
|
|
108
|
-
disabled
|
|
109
|
-
className={`${baseStyles} opacity-60 cursor-not-allowed`}
|
|
110
|
-
>
|
|
111
|
-
Discord (Disabled)
|
|
112
|
-
</OAuthButton>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
export const Loading: Story = () => (
|
|
117
|
-
<div className="flex flex-col gap-4">
|
|
118
|
-
<OAuthButton provider="google" loading className={baseStyles}>
|
|
119
|
-
Google (Loading)
|
|
120
|
-
</OAuthButton>
|
|
121
|
-
<OAuthButton provider="discord" loading className={baseStyles}>
|
|
122
|
-
Discord (Loading)
|
|
123
|
-
</OAuthButton>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
75
|
+
export const AllVariants: Story = () => (
|
|
76
|
+
<div className="flex flex-col gap-4 items-start">
|
|
77
|
+
<div className="space-y-2">
|
|
78
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Primary</p>
|
|
79
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
80
|
+
<OAuthButton provider="google" variant="primary" />
|
|
81
|
+
<OAuthButton provider="discord" variant="primary" />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
126
84
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</OAuthButton>
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Secondary (Default)</p>
|
|
87
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
88
|
+
<OAuthButton provider="google" variant="secondary" />
|
|
89
|
+
<OAuthButton provider="discord" variant="secondary" />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
135
92
|
</div>
|
|
136
93
|
);
|
|
137
94
|
|
|
138
|
-
export const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<div className="flex gap-4">
|
|
144
|
-
<OAuthButton
|
|
145
|
-
provider="google"
|
|
146
|
-
onMouseDown={p1.onMouseDown}
|
|
147
|
-
onMouseUp={p1.onMouseUp}
|
|
148
|
-
onMouseLeave={p1.onMouseLeave}
|
|
149
|
-
className={`p-3 rounded-full bg-white border border-gray-300 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:shadow transition-all duration-75 cursor-pointer ${p1.className}`}
|
|
150
|
-
aria-label="Sign in with Google"
|
|
151
|
-
>
|
|
152
|
-
{null}
|
|
153
|
-
</OAuthButton>
|
|
154
|
-
<OAuthButton
|
|
155
|
-
provider="discord"
|
|
156
|
-
onMouseDown={p2.onMouseDown}
|
|
157
|
-
onMouseUp={p2.onMouseUp}
|
|
158
|
-
onMouseLeave={p2.onMouseLeave}
|
|
159
|
-
className={`p-3 rounded-full bg-white border border-gray-300 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:shadow transition-all duration-75 cursor-pointer ${p2.className}`}
|
|
160
|
-
aria-label="Sign in with Discord"
|
|
161
|
-
>
|
|
162
|
-
{null}
|
|
163
|
-
</OAuthButton>
|
|
95
|
+
export const States: Story = () => (
|
|
96
|
+
<div className="flex flex-col gap-4 items-start">
|
|
97
|
+
<div className="space-y-2">
|
|
98
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Default</p>
|
|
99
|
+
<OAuthButton provider="google" />
|
|
164
100
|
</div>
|
|
165
|
-
);
|
|
166
|
-
};
|
|
167
101
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const g2 = useRealPress();
|
|
172
|
-
const d2 = useRealPress();
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div className="flex flex-col gap-6">
|
|
176
|
-
<div className="flex items-center gap-4">
|
|
177
|
-
<OAuthButton
|
|
178
|
-
provider="google"
|
|
179
|
-
onMouseDown={g1.onMouseDown}
|
|
180
|
-
onMouseUp={g1.onMouseUp}
|
|
181
|
-
onMouseLeave={g1.onMouseLeave}
|
|
182
|
-
className={`size-12 rounded-full bg-white border border-gray-300 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:shadow transition-all duration-75 cursor-pointer flex items-center justify-center ${g1.className}`}
|
|
183
|
-
aria-label="Sign in with Google"
|
|
184
|
-
>
|
|
185
|
-
{null}
|
|
186
|
-
</OAuthButton>
|
|
187
|
-
<OAuthButton
|
|
188
|
-
provider="discord"
|
|
189
|
-
onMouseDown={d1.onMouseDown}
|
|
190
|
-
onMouseUp={d1.onMouseUp}
|
|
191
|
-
onMouseLeave={d1.onMouseLeave}
|
|
192
|
-
className={`size-12 rounded-full bg-white border border-gray-300 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:shadow transition-all duration-75 cursor-pointer flex items-center justify-center ${d1.className}`}
|
|
193
|
-
aria-label="Sign in with Discord"
|
|
194
|
-
>
|
|
195
|
-
{null}
|
|
196
|
-
</OAuthButton>
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
<div className="flex items-center gap-4">
|
|
200
|
-
<OAuthButton
|
|
201
|
-
provider="google"
|
|
202
|
-
onMouseDown={g2.onMouseDown}
|
|
203
|
-
onMouseUp={g2.onMouseUp}
|
|
204
|
-
onMouseLeave={g2.onMouseLeave}
|
|
205
|
-
className={`size-14 rounded-full bg-gray-900 text-white border border-gray-800 shadow-md hover:bg-gray-800 hover:shadow-lg transition-all duration-75 cursor-pointer flex items-center justify-center ${g2.className}`}
|
|
206
|
-
aria-label="Sign in with Google"
|
|
207
|
-
>
|
|
208
|
-
{null}
|
|
209
|
-
</OAuthButton>
|
|
210
|
-
<OAuthButton
|
|
211
|
-
provider="discord"
|
|
212
|
-
onMouseDown={d2.onMouseDown}
|
|
213
|
-
onMouseUp={d2.onMouseUp}
|
|
214
|
-
onMouseLeave={d2.onMouseLeave}
|
|
215
|
-
className={`size-14 rounded-full bg-indigo-600 text-white border border-indigo-500 shadow-md hover:bg-indigo-500 hover:shadow-lg transition-all duration-75 cursor-pointer flex items-center justify-center ${d2.className}`}
|
|
216
|
-
aria-label="Sign in with Discord"
|
|
217
|
-
>
|
|
218
|
-
{null}
|
|
219
|
-
</OAuthButton>
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
<div className="flex items-center gap-4">
|
|
223
|
-
<OAuthButton
|
|
224
|
-
provider="google"
|
|
225
|
-
loading
|
|
226
|
-
className="size-12 rounded-full bg-white border border-gray-300 shadow-sm cursor-wait flex items-center justify-center"
|
|
227
|
-
aria-label="Connecting with Google"
|
|
228
|
-
>
|
|
229
|
-
{null}
|
|
230
|
-
</OAuthButton>
|
|
231
|
-
<OAuthButton
|
|
232
|
-
provider="discord"
|
|
233
|
-
disabled
|
|
234
|
-
className="size-12 rounded-full bg-gray-100 border border-gray-300 opacity-60 cursor-not-allowed flex items-center justify-center"
|
|
235
|
-
aria-label="Sign in with Discord"
|
|
236
|
-
>
|
|
237
|
-
{null}
|
|
238
|
-
</OAuthButton>
|
|
239
|
-
</div>
|
|
102
|
+
<div className="space-y-2">
|
|
103
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Disabled</p>
|
|
104
|
+
<OAuthButton provider="google" disabled />
|
|
240
105
|
</div>
|
|
241
|
-
);
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
export const CustomStyling: Story = () => {
|
|
245
|
-
const g = useRealPress();
|
|
246
|
-
const d = useRealPress();
|
|
247
106
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
<OAuthButton
|
|
251
|
-
provider="google"
|
|
252
|
-
onMouseDown={g.onMouseDown}
|
|
253
|
-
onMouseUp={g.onMouseUp}
|
|
254
|
-
onMouseLeave={g.onMouseLeave}
|
|
255
|
-
className={`flex items-center gap-3 px-8 py-4 bg-red-600 text-white rounded-full font-semibold shadow-lg hover:bg-red-700 hover:shadow-xl transition-all duration-75 cursor-pointer ${g.className}`}
|
|
256
|
-
>
|
|
257
|
-
Continue with Google
|
|
258
|
-
</OAuthButton>
|
|
259
|
-
<OAuthButton
|
|
260
|
-
provider="discord"
|
|
261
|
-
onMouseDown={d.onMouseDown}
|
|
262
|
-
onMouseUp={d.onMouseUp}
|
|
263
|
-
onMouseLeave={d.onMouseLeave}
|
|
264
|
-
className={`flex items-center gap-3 px-8 py-4 bg-indigo-600 text-white rounded-lg font-semibold shadow-lg hover:bg-indigo-700 hover:shadow-xl transition-all duration-75 cursor-pointer ${d.className}`}
|
|
265
|
-
>
|
|
266
|
-
Join via Discord
|
|
267
|
-
</OAuthButton>
|
|
107
|
+
<div className="space-y-2">
|
|
108
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Loading</p>
|
|
109
|
+
<OAuthButton provider="google" loading />
|
|
268
110
|
</div>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const circularBaseStyles =
|
|
273
|
-
"size-12 rounded-full bg-white border border-gray-300 shadow-sm hover:bg-gray-50 hover:border-gray-400 hover:shadow transition-all duration-75 cursor-pointer flex items-center justify-center";
|
|
274
|
-
|
|
275
|
-
const circularDarkStyles =
|
|
276
|
-
"dark:bg-gray-900 dark:border-gray-700 dark:hover:bg-gray-800";
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
277
113
|
|
|
278
|
-
export const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const dCirc = useRealPress();
|
|
114
|
+
export const DarkMode: Story = () => (
|
|
115
|
+
<div className="dark bg-[var(--sublime-color-surface-0)] p-8 rounded-lg">
|
|
116
|
+
<div className="flex flex-col gap-4 items-start">
|
|
117
|
+
<p className="text-[var(--sublime-color-text-primary)] mb-4">Dark Mode Preview</p>
|
|
283
118
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<
|
|
287
|
-
<p className="text-sm font-medium text-gray-500">Google</p>
|
|
288
|
-
<OAuthButton
|
|
289
|
-
provider="google"
|
|
290
|
-
onMouseDown={g.onMouseDown}
|
|
291
|
-
onMouseUp={g.onMouseUp}
|
|
292
|
-
onMouseLeave={g.onMouseLeave}
|
|
293
|
-
className={`${baseStyles} ${darkStyles} ${g.className}`}
|
|
294
|
-
>
|
|
295
|
-
Default
|
|
296
|
-
</OAuthButton>
|
|
297
|
-
<OAuthButton provider="google" disabled className={baseStyles}>
|
|
298
|
-
Disabled
|
|
299
|
-
</OAuthButton>
|
|
300
|
-
<OAuthButton provider="google" loading className={baseStyles}>
|
|
301
|
-
Loading
|
|
302
|
-
</OAuthButton>
|
|
303
|
-
</div>
|
|
304
|
-
<div className="space-y-2">
|
|
305
|
-
<p className="text-sm font-medium text-gray-500">Discord</p>
|
|
306
|
-
<OAuthButton
|
|
307
|
-
provider="discord"
|
|
308
|
-
onMouseDown={d.onMouseDown}
|
|
309
|
-
onMouseUp={d.onMouseUp}
|
|
310
|
-
onMouseLeave={d.onMouseLeave}
|
|
311
|
-
className={`${baseStyles} ${darkStyles} ${d.className}`}
|
|
312
|
-
>
|
|
313
|
-
Default
|
|
314
|
-
</OAuthButton>
|
|
315
|
-
<OAuthButton provider="discord" disabled className={baseStyles}>
|
|
316
|
-
Disabled
|
|
317
|
-
</OAuthButton>
|
|
318
|
-
<OAuthButton provider="discord" loading className={baseStyles}>
|
|
319
|
-
Loading
|
|
320
|
-
</OAuthButton>
|
|
321
|
-
</div>
|
|
322
|
-
<div className="space-y-2">
|
|
323
|
-
<p className="text-sm font-medium text-gray-500">Circular Icons</p>
|
|
324
|
-
<div className="flex items-center gap-2">
|
|
325
|
-
<OAuthButton
|
|
326
|
-
provider="google"
|
|
327
|
-
onMouseDown={gCirc.onMouseDown}
|
|
328
|
-
onMouseUp={gCirc.onMouseUp}
|
|
329
|
-
onMouseLeave={gCirc.onMouseLeave}
|
|
330
|
-
className={`${circularBaseStyles} ${circularDarkStyles} ${gCirc.className}`}
|
|
331
|
-
aria-label="Sign in with Google"
|
|
332
|
-
>
|
|
333
|
-
{null}
|
|
334
|
-
</OAuthButton>
|
|
335
|
-
<OAuthButton
|
|
336
|
-
provider="discord"
|
|
337
|
-
onMouseDown={dCirc.onMouseDown}
|
|
338
|
-
onMouseUp={dCirc.onMouseUp}
|
|
339
|
-
onMouseLeave={dCirc.onMouseLeave}
|
|
340
|
-
className={`${circularBaseStyles} ${circularDarkStyles} ${dCirc.className}`}
|
|
341
|
-
aria-label="Sign in with Discord"
|
|
342
|
-
>
|
|
343
|
-
{null}
|
|
344
|
-
</OAuthButton>
|
|
345
|
-
</div>
|
|
346
|
-
<div className="flex items-center gap-2">
|
|
347
|
-
<OAuthButton
|
|
348
|
-
provider="google"
|
|
349
|
-
disabled
|
|
350
|
-
className={`${circularBaseStyles} opacity-60 cursor-not-allowed`}
|
|
351
|
-
aria-label="Sign in with Google"
|
|
352
|
-
>
|
|
353
|
-
{null}
|
|
354
|
-
</OAuthButton>
|
|
355
|
-
<OAuthButton
|
|
356
|
-
provider="discord"
|
|
357
|
-
disabled
|
|
358
|
-
className={`${circularBaseStyles} opacity-60 cursor-not-allowed`}
|
|
359
|
-
aria-label="Sign in with Discord"
|
|
360
|
-
>
|
|
361
|
-
{null}
|
|
362
|
-
</OAuthButton>
|
|
363
|
-
</div>
|
|
364
|
-
<div className="flex items-center gap-2">
|
|
365
|
-
<OAuthButton
|
|
366
|
-
provider="google"
|
|
367
|
-
loading
|
|
368
|
-
className={`${circularBaseStyles} cursor-wait`}
|
|
369
|
-
aria-label="Connecting with Google"
|
|
370
|
-
>
|
|
371
|
-
{null}
|
|
372
|
-
</OAuthButton>
|
|
373
|
-
<OAuthButton
|
|
374
|
-
provider="discord"
|
|
375
|
-
loading
|
|
376
|
-
className={`${circularBaseStyles} cursor-wait`}
|
|
377
|
-
aria-label="Connecting with Discord"
|
|
378
|
-
>
|
|
379
|
-
{null}
|
|
380
|
-
</OAuthButton>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
119
|
+
<OAuthButton provider="google" variant="primary" />
|
|
120
|
+
<OAuthButton provider="google" variant="secondary" />
|
|
121
|
+
<OAuthButton provider="discord" variant="secondary" loading />
|
|
383
122
|
</div>
|
|
384
|
-
|
|
385
|
-
|
|
123
|
+
</div>
|
|
124
|
+
);
|
package/src/oauth-button.tsx
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { BaseButton } from './base-button';
|
|
6
|
+
import { DiscordIcon, GoogleIcon } from './icons';
|
|
5
7
|
import type { OAuthButtonProps, OAuthProvider } from './types';
|
|
6
|
-
import { GoogleIcon, DiscordIcon, SpinnerIcon } from './icons';
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
* Maps OAuth providers to their display names
|
|
10
|
-
*/
|
|
11
9
|
const PROVIDER_NAMES: Record<OAuthProvider, string> = {
|
|
12
10
|
google: 'Google',
|
|
13
11
|
discord: 'Discord',
|
|
@@ -18,108 +16,65 @@ const PROVIDER_ICONS: Record<OAuthProvider, typeof GoogleIcon> = {
|
|
|
18
16
|
discord: DiscordIcon,
|
|
19
17
|
};
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* These are UX fundamentals that don't affect visual design.
|
|
24
|
-
*/
|
|
25
|
-
const SENSIBLE_DEFAULTS: readonly string[] = ['cursor-pointer'];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Merges sensible default classes with user-provided classes.
|
|
29
|
-
* Prevents duplicate cursor classes if user already specified them.
|
|
30
|
-
*/
|
|
31
|
-
function mergeButtonClasses(userClassName: string | undefined, isDisabled: boolean): string {
|
|
32
|
-
const userClasses = userClassName?.trim() ?? '';
|
|
33
|
-
|
|
34
|
-
// Filter out defaults that user already specified to avoid duplicates
|
|
35
|
-
const neededDefaults = SENSIBLE_DEFAULTS.filter(
|
|
36
|
-
defaultClass => !userClasses.includes(defaultClass)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// Add disabled cursor class if needed and not already present
|
|
40
|
-
if (isDisabled && !userClasses.includes('cursor-not-allowed')) {
|
|
41
|
-
neededDefaults.push('cursor-not-allowed');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (neededDefaults.length === 0) {
|
|
45
|
-
return userClasses;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return `${neededDefaults.join(' ')} ${userClasses}`.trim();
|
|
19
|
+
function mergeClassNames(...classNames: Array<string | undefined>): string {
|
|
20
|
+
return classNames.filter(Boolean).join(' ');
|
|
49
21
|
}
|
|
50
22
|
|
|
51
23
|
/**
|
|
52
24
|
* OAuthButton component
|
|
53
25
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Design Philosophy:
|
|
59
|
-
* - We add sensible UX defaults (cursor-pointer, proper disabled states)
|
|
60
|
-
* - We don't impose visual opinions - you control all styling via className
|
|
61
|
-
* - The result is a solid, accessible foundation that looks exactly how you want
|
|
26
|
+
* Built on top of the shared Sublime button foundation so it inherits
|
|
27
|
+
* runtime styling, semantic variants, curated press motion, and loading
|
|
28
|
+
* behavior by default.
|
|
62
29
|
*
|
|
63
30
|
* @example
|
|
64
31
|
* ```tsx
|
|
65
|
-
*
|
|
66
|
-
* <OAuthButton
|
|
67
|
-
* provider="google"
|
|
68
|
-
* onClick={signInWithGoogle}
|
|
69
|
-
* className="flex items-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg font-medium"
|
|
70
|
-
* />
|
|
71
|
-
*
|
|
72
|
-
* // Icon only - pass {null} as children
|
|
73
|
-
* <OAuthButton
|
|
74
|
-
* provider="google"
|
|
75
|
-
* className="p-3 rounded-full hover:bg-white/10"
|
|
76
|
-
* >
|
|
77
|
-
* {null}
|
|
78
|
-
* </OAuthButton>
|
|
79
|
-
*
|
|
80
|
-
* // Loading state - cursor-not-allowed is automatic
|
|
81
|
-
* <OAuthButton provider="google" loading className="opacity-50" />
|
|
32
|
+
* <OAuthButton provider="google" />
|
|
33
|
+
* <OAuthButton provider="discord" variant="primary" loading />
|
|
82
34
|
* ```
|
|
83
35
|
*/
|
|
84
36
|
export const OAuthButton = forwardRef<HTMLButtonElement, OAuthButtonProps>(
|
|
85
37
|
function OAuthButton(props, forwardedRef) {
|
|
86
38
|
const {
|
|
39
|
+
'aria-label': ariaLabel,
|
|
87
40
|
provider,
|
|
88
41
|
onClick,
|
|
89
42
|
loading = false,
|
|
43
|
+
loadingAnimation = 'spinner',
|
|
44
|
+
disabled = false,
|
|
45
|
+
variant = 'secondary',
|
|
46
|
+
animation = 'press',
|
|
90
47
|
className,
|
|
91
48
|
children,
|
|
92
|
-
disabled = false,
|
|
93
49
|
...otherProps
|
|
94
50
|
} = props;
|
|
95
51
|
|
|
96
|
-
const
|
|
52
|
+
const isIconOnly = children === null;
|
|
97
53
|
const Icon = PROVIDER_ICONS[provider];
|
|
98
54
|
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
99
|
-
const
|
|
55
|
+
const buttonLabel = loading
|
|
56
|
+
? children ?? 'Conectando...'
|
|
57
|
+
: children ?? defaultText;
|
|
58
|
+
const resolvedAriaLabel = ariaLabel ?? (isIconOnly ? defaultText : undefined);
|
|
100
59
|
|
|
101
60
|
return (
|
|
102
|
-
<
|
|
61
|
+
<BaseButton
|
|
103
62
|
ref={forwardedRef}
|
|
104
63
|
onClick={onClick}
|
|
105
|
-
disabled={
|
|
106
|
-
|
|
107
|
-
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
animation={animation}
|
|
66
|
+
variant={variant}
|
|
67
|
+
loading={loading}
|
|
68
|
+
loadingAnimation={loadingAnimation}
|
|
69
|
+
isIconOnly={isIconOnly}
|
|
70
|
+
leadingVisual={<Icon size={20} />}
|
|
71
|
+
className={mergeClassNames('sublime-auth-button', className)}
|
|
72
|
+
data-provider={provider}
|
|
73
|
+
aria-label={resolvedAriaLabel}
|
|
108
74
|
{...otherProps}
|
|
109
75
|
>
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
<SpinnerIcon />
|
|
113
|
-
{children !== null && <span>{children ?? 'Conectando...'}</span>}
|
|
114
|
-
</>
|
|
115
|
-
) : (
|
|
116
|
-
<>
|
|
117
|
-
<Icon />
|
|
118
|
-
{children !== null && <span>{children ?? defaultText}</span>}
|
|
119
|
-
</>
|
|
120
|
-
)}
|
|
121
|
-
</Button>
|
|
76
|
+
{buttonLabel}
|
|
77
|
+
</BaseButton>
|
|
122
78
|
);
|
|
123
79
|
}
|
|
124
|
-
);
|
|
125
|
-
|
|
80
|
+
);
|