@sublimee/auth-ui 0.1.0 → 0.1.1
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/dist/index.d.mts +6 -14
- package/dist/index.d.ts +6 -14
- package/dist/index.js +15 -31
- package/dist/index.mjs +16 -32
- package/package.json +1 -1
- package/src/icons.tsx +10 -16
- package/src/index.ts +0 -1
- package/src/oauth-button.stories.tsx +385 -0
- package/src/oauth-button.tsx +7 -21
- package/src/types.ts +2 -12
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
type OAuthProvider = 'google' | 'discord';
|
|
6
|
-
interface OAuthButtonProps {
|
|
6
|
+
interface OAuthButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
|
7
7
|
/**
|
|
8
8
|
* The OAuth provider to authenticate with
|
|
9
9
|
*/
|
|
@@ -26,10 +26,6 @@ interface OAuthButtonProps {
|
|
|
26
26
|
* Pass `null` for icon-only mode
|
|
27
27
|
*/
|
|
28
28
|
children?: ReactNode;
|
|
29
|
-
/**
|
|
30
|
-
* Disable the button
|
|
31
|
-
*/
|
|
32
|
-
disabled?: boolean;
|
|
33
29
|
}
|
|
34
30
|
interface OAuthIconsProps {
|
|
35
31
|
/**
|
|
@@ -37,10 +33,6 @@ interface OAuthIconsProps {
|
|
|
37
33
|
* @default 24
|
|
38
34
|
*/
|
|
39
35
|
size?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Custom className
|
|
42
|
-
*/
|
|
43
|
-
className?: string;
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
/**
|
|
@@ -82,15 +74,15 @@ declare const OAuthButton: react.ForwardRefExoticComponent<OAuthButtonProps & re
|
|
|
82
74
|
* Google Logo Icon
|
|
83
75
|
* Official Google "G" logo for OAuth authentication
|
|
84
76
|
*/
|
|
85
|
-
declare function GoogleIcon({ size
|
|
77
|
+
declare function GoogleIcon({ size }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
86
78
|
/**
|
|
87
79
|
* Discord Logo Icon
|
|
88
80
|
* Official Discord logo for OAuth authentication
|
|
89
81
|
*/
|
|
90
|
-
declare function DiscordIcon({ size
|
|
82
|
+
declare function DiscordIcon({ size }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
91
83
|
/**
|
|
92
84
|
* Spinner Icon for loading state
|
|
93
85
|
*/
|
|
94
|
-
declare function SpinnerIcon({ size
|
|
86
|
+
declare function SpinnerIcon({ size }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
95
87
|
|
|
96
|
-
export { DiscordIcon, GoogleIcon, OAuthButton, type OAuthButtonProps, type OAuthIconsProps, type OAuthProvider, SpinnerIcon
|
|
88
|
+
export { DiscordIcon, GoogleIcon, OAuthButton, type OAuthButtonProps, type OAuthIconsProps, type OAuthProvider, SpinnerIcon };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
type OAuthProvider = 'google' | 'discord';
|
|
6
|
-
interface OAuthButtonProps {
|
|
6
|
+
interface OAuthButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
|
7
7
|
/**
|
|
8
8
|
* The OAuth provider to authenticate with
|
|
9
9
|
*/
|
|
@@ -26,10 +26,6 @@ interface OAuthButtonProps {
|
|
|
26
26
|
* Pass `null` for icon-only mode
|
|
27
27
|
*/
|
|
28
28
|
children?: ReactNode;
|
|
29
|
-
/**
|
|
30
|
-
* Disable the button
|
|
31
|
-
*/
|
|
32
|
-
disabled?: boolean;
|
|
33
29
|
}
|
|
34
30
|
interface OAuthIconsProps {
|
|
35
31
|
/**
|
|
@@ -37,10 +33,6 @@ interface OAuthIconsProps {
|
|
|
37
33
|
* @default 24
|
|
38
34
|
*/
|
|
39
35
|
size?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Custom className
|
|
42
|
-
*/
|
|
43
|
-
className?: string;
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
/**
|
|
@@ -82,15 +74,15 @@ declare const OAuthButton: react.ForwardRefExoticComponent<OAuthButtonProps & re
|
|
|
82
74
|
* Google Logo Icon
|
|
83
75
|
* Official Google "G" logo for OAuth authentication
|
|
84
76
|
*/
|
|
85
|
-
declare function GoogleIcon({ size
|
|
77
|
+
declare function GoogleIcon({ size }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
86
78
|
/**
|
|
87
79
|
* Discord Logo Icon
|
|
88
80
|
* Official Discord logo for OAuth authentication
|
|
89
81
|
*/
|
|
90
|
-
declare function DiscordIcon({ size
|
|
82
|
+
declare function DiscordIcon({ size }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
91
83
|
/**
|
|
92
84
|
* Spinner Icon for loading state
|
|
93
85
|
*/
|
|
94
|
-
declare function SpinnerIcon({ size
|
|
86
|
+
declare function SpinnerIcon({ size }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
95
87
|
|
|
96
|
-
export { DiscordIcon, GoogleIcon, OAuthButton, type OAuthButtonProps, type OAuthIconsProps, type OAuthProvider, SpinnerIcon
|
|
88
|
+
export { DiscordIcon, GoogleIcon, OAuthButton, type OAuthButtonProps, type OAuthIconsProps, type OAuthProvider, SpinnerIcon };
|
package/dist/index.js
CHANGED
|
@@ -23,8 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
DiscordIcon: () => DiscordIcon,
|
|
24
24
|
GoogleIcon: () => GoogleIcon,
|
|
25
25
|
OAuthButton: () => OAuthButton,
|
|
26
|
-
SpinnerIcon: () => SpinnerIcon
|
|
27
|
-
default: () => oauth_button_default
|
|
26
|
+
SpinnerIcon: () => SpinnerIcon
|
|
28
27
|
});
|
|
29
28
|
module.exports = __toCommonJS(index_exports);
|
|
30
29
|
|
|
@@ -34,11 +33,10 @@ var import_button = require("@base-ui/react/button");
|
|
|
34
33
|
|
|
35
34
|
// src/icons.tsx
|
|
36
35
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
37
|
-
function getSafeSize(size) {
|
|
38
|
-
|
|
39
|
-
return Math.max(1, safeSize);
|
|
36
|
+
function getSafeSize(size, defaultSize = 24) {
|
|
37
|
+
return Math.max(1, size ?? defaultSize);
|
|
40
38
|
}
|
|
41
|
-
function GoogleIcon({ size
|
|
39
|
+
function GoogleIcon({ size }) {
|
|
42
40
|
const safeSize = getSafeSize(size);
|
|
43
41
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
44
42
|
"svg",
|
|
@@ -48,7 +46,6 @@ function GoogleIcon({ size, className }) {
|
|
|
48
46
|
viewBox: "0 0 24 24",
|
|
49
47
|
fill: "none",
|
|
50
48
|
xmlns: "http://www.w3.org/2000/svg",
|
|
51
|
-
className,
|
|
52
49
|
"aria-hidden": "true",
|
|
53
50
|
children: [
|
|
54
51
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -83,7 +80,7 @@ function GoogleIcon({ size, className }) {
|
|
|
83
80
|
}
|
|
84
81
|
);
|
|
85
82
|
}
|
|
86
|
-
function DiscordIcon({ size
|
|
83
|
+
function DiscordIcon({ size }) {
|
|
87
84
|
const safeSize = getSafeSize(size);
|
|
88
85
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
89
86
|
"svg",
|
|
@@ -93,7 +90,6 @@ function DiscordIcon({ size, className }) {
|
|
|
93
90
|
viewBox: "0 0 24 24",
|
|
94
91
|
fill: "none",
|
|
95
92
|
xmlns: "http://www.w3.org/2000/svg",
|
|
96
|
-
className,
|
|
97
93
|
"aria-hidden": "true",
|
|
98
94
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
95
|
"path",
|
|
@@ -105,18 +101,16 @@ function DiscordIcon({ size, className }) {
|
|
|
105
101
|
}
|
|
106
102
|
);
|
|
107
103
|
}
|
|
108
|
-
function SpinnerIcon({ size
|
|
109
|
-
const safeSize = size
|
|
110
|
-
const finalSize = Math.max(1, safeSize);
|
|
104
|
+
function SpinnerIcon({ size }) {
|
|
105
|
+
const safeSize = getSafeSize(size, 20);
|
|
111
106
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
112
107
|
"svg",
|
|
113
108
|
{
|
|
114
|
-
width:
|
|
115
|
-
height:
|
|
109
|
+
width: safeSize,
|
|
110
|
+
height: safeSize,
|
|
116
111
|
viewBox: "0 0 24 24",
|
|
117
112
|
fill: "none",
|
|
118
113
|
xmlns: "http://www.w3.org/2000/svg",
|
|
119
|
-
className,
|
|
120
114
|
"aria-hidden": "true",
|
|
121
115
|
children: [
|
|
122
116
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -164,16 +158,10 @@ var PROVIDER_NAMES = {
|
|
|
164
158
|
google: "Google",
|
|
165
159
|
discord: "Discord"
|
|
166
160
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
case "discord":
|
|
172
|
-
return DiscordIcon;
|
|
173
|
-
default:
|
|
174
|
-
return () => null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
161
|
+
var PROVIDER_ICONS = {
|
|
162
|
+
google: GoogleIcon,
|
|
163
|
+
discord: DiscordIcon
|
|
164
|
+
};
|
|
177
165
|
var SENSIBLE_DEFAULTS = ["cursor-pointer"];
|
|
178
166
|
function mergeButtonClasses(userClassName, isDisabled) {
|
|
179
167
|
const userClasses = userClassName?.trim() ?? "";
|
|
@@ -200,12 +188,9 @@ var OAuthButton = (0, import_react.forwardRef)(
|
|
|
200
188
|
...otherProps
|
|
201
189
|
} = props;
|
|
202
190
|
const isDisabled = disabled || loading;
|
|
203
|
-
const Icon =
|
|
191
|
+
const Icon = PROVIDER_ICONS[provider];
|
|
204
192
|
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
205
|
-
const mergedClassName = (
|
|
206
|
-
() => mergeButtonClasses(className, isDisabled),
|
|
207
|
-
[className, isDisabled]
|
|
208
|
-
);
|
|
193
|
+
const mergedClassName = mergeButtonClasses(className, isDisabled);
|
|
209
194
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
210
195
|
import_button.Button,
|
|
211
196
|
{
|
|
@@ -226,7 +211,6 @@ var OAuthButton = (0, import_react.forwardRef)(
|
|
|
226
211
|
);
|
|
227
212
|
}
|
|
228
213
|
);
|
|
229
|
-
var oauth_button_default = OAuthButton;
|
|
230
214
|
// Annotate the CommonJS export names for ESM import in node:
|
|
231
215
|
0 && (module.exports = {
|
|
232
216
|
DiscordIcon,
|
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// src/oauth-button.tsx
|
|
2
|
-
import { forwardRef
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
3
|
import { Button } from "@base-ui/react/button";
|
|
4
4
|
|
|
5
5
|
// src/icons.tsx
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
function getSafeSize(size) {
|
|
8
|
-
|
|
9
|
-
return Math.max(1, safeSize);
|
|
7
|
+
function getSafeSize(size, defaultSize = 24) {
|
|
8
|
+
return Math.max(1, size ?? defaultSize);
|
|
10
9
|
}
|
|
11
|
-
function GoogleIcon({ size
|
|
10
|
+
function GoogleIcon({ size }) {
|
|
12
11
|
const safeSize = getSafeSize(size);
|
|
13
12
|
return /* @__PURE__ */ jsxs(
|
|
14
13
|
"svg",
|
|
@@ -18,7 +17,6 @@ function GoogleIcon({ size, className }) {
|
|
|
18
17
|
viewBox: "0 0 24 24",
|
|
19
18
|
fill: "none",
|
|
20
19
|
xmlns: "http://www.w3.org/2000/svg",
|
|
21
|
-
className,
|
|
22
20
|
"aria-hidden": "true",
|
|
23
21
|
children: [
|
|
24
22
|
/* @__PURE__ */ jsx(
|
|
@@ -53,7 +51,7 @@ function GoogleIcon({ size, className }) {
|
|
|
53
51
|
}
|
|
54
52
|
);
|
|
55
53
|
}
|
|
56
|
-
function DiscordIcon({ size
|
|
54
|
+
function DiscordIcon({ size }) {
|
|
57
55
|
const safeSize = getSafeSize(size);
|
|
58
56
|
return /* @__PURE__ */ jsx(
|
|
59
57
|
"svg",
|
|
@@ -63,7 +61,6 @@ function DiscordIcon({ size, className }) {
|
|
|
63
61
|
viewBox: "0 0 24 24",
|
|
64
62
|
fill: "none",
|
|
65
63
|
xmlns: "http://www.w3.org/2000/svg",
|
|
66
|
-
className,
|
|
67
64
|
"aria-hidden": "true",
|
|
68
65
|
children: /* @__PURE__ */ jsx(
|
|
69
66
|
"path",
|
|
@@ -75,18 +72,16 @@ function DiscordIcon({ size, className }) {
|
|
|
75
72
|
}
|
|
76
73
|
);
|
|
77
74
|
}
|
|
78
|
-
function SpinnerIcon({ size
|
|
79
|
-
const safeSize = size
|
|
80
|
-
const finalSize = Math.max(1, safeSize);
|
|
75
|
+
function SpinnerIcon({ size }) {
|
|
76
|
+
const safeSize = getSafeSize(size, 20);
|
|
81
77
|
return /* @__PURE__ */ jsxs(
|
|
82
78
|
"svg",
|
|
83
79
|
{
|
|
84
|
-
width:
|
|
85
|
-
height:
|
|
80
|
+
width: safeSize,
|
|
81
|
+
height: safeSize,
|
|
86
82
|
viewBox: "0 0 24 24",
|
|
87
83
|
fill: "none",
|
|
88
84
|
xmlns: "http://www.w3.org/2000/svg",
|
|
89
|
-
className,
|
|
90
85
|
"aria-hidden": "true",
|
|
91
86
|
children: [
|
|
92
87
|
/* @__PURE__ */ jsx(
|
|
@@ -134,16 +129,10 @@ var PROVIDER_NAMES = {
|
|
|
134
129
|
google: "Google",
|
|
135
130
|
discord: "Discord"
|
|
136
131
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
case "discord":
|
|
142
|
-
return DiscordIcon;
|
|
143
|
-
default:
|
|
144
|
-
return () => null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
132
|
+
var PROVIDER_ICONS = {
|
|
133
|
+
google: GoogleIcon,
|
|
134
|
+
discord: DiscordIcon
|
|
135
|
+
};
|
|
147
136
|
var SENSIBLE_DEFAULTS = ["cursor-pointer"];
|
|
148
137
|
function mergeButtonClasses(userClassName, isDisabled) {
|
|
149
138
|
const userClasses = userClassName?.trim() ?? "";
|
|
@@ -170,12 +159,9 @@ var OAuthButton = forwardRef(
|
|
|
170
159
|
...otherProps
|
|
171
160
|
} = props;
|
|
172
161
|
const isDisabled = disabled || loading;
|
|
173
|
-
const Icon =
|
|
162
|
+
const Icon = PROVIDER_ICONS[provider];
|
|
174
163
|
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
175
|
-
const mergedClassName =
|
|
176
|
-
() => mergeButtonClasses(className, isDisabled),
|
|
177
|
-
[className, isDisabled]
|
|
178
|
-
);
|
|
164
|
+
const mergedClassName = mergeButtonClasses(className, isDisabled);
|
|
179
165
|
return /* @__PURE__ */ jsx2(
|
|
180
166
|
Button,
|
|
181
167
|
{
|
|
@@ -196,11 +182,9 @@ var OAuthButton = forwardRef(
|
|
|
196
182
|
);
|
|
197
183
|
}
|
|
198
184
|
);
|
|
199
|
-
var oauth_button_default = OAuthButton;
|
|
200
185
|
export {
|
|
201
186
|
DiscordIcon,
|
|
202
187
|
GoogleIcon,
|
|
203
188
|
OAuthButton,
|
|
204
|
-
SpinnerIcon
|
|
205
|
-
oauth_button_default as default
|
|
189
|
+
SpinnerIcon
|
|
206
190
|
};
|
package/package.json
CHANGED
package/src/icons.tsx
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import type { OAuthIconsProps } from './types';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Validates and sanitizes icon size (minimum 1px)
|
|
7
5
|
*/
|
|
8
|
-
function getSafeSize(size: number | undefined): number {
|
|
9
|
-
|
|
10
|
-
return Math.max(1, safeSize);
|
|
6
|
+
function getSafeSize(size: number | undefined, defaultSize = 24): number {
|
|
7
|
+
return Math.max(1, size ?? defaultSize);
|
|
11
8
|
}
|
|
12
9
|
|
|
13
10
|
/**
|
|
14
11
|
* Google Logo Icon
|
|
15
12
|
* Official Google "G" logo for OAuth authentication
|
|
16
13
|
*/
|
|
17
|
-
export function GoogleIcon({ size
|
|
14
|
+
export function GoogleIcon({ size }: OAuthIconsProps) {
|
|
18
15
|
const safeSize = getSafeSize(size);
|
|
16
|
+
|
|
19
17
|
return (
|
|
20
18
|
<svg
|
|
21
19
|
width={safeSize}
|
|
@@ -23,9 +21,9 @@ export function GoogleIcon({ size, className }: OAuthIconsProps) {
|
|
|
23
21
|
viewBox="0 0 24 24"
|
|
24
22
|
fill="none"
|
|
25
23
|
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
-
className={className}
|
|
27
24
|
aria-hidden="true"
|
|
28
25
|
>
|
|
26
|
+
{/* Colored G paths */}
|
|
29
27
|
<path
|
|
30
28
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
31
29
|
fill="#4285F4"
|
|
@@ -50,7 +48,7 @@ export function GoogleIcon({ size, className }: OAuthIconsProps) {
|
|
|
50
48
|
* Discord Logo Icon
|
|
51
49
|
* Official Discord logo for OAuth authentication
|
|
52
50
|
*/
|
|
53
|
-
export function DiscordIcon({ size
|
|
51
|
+
export function DiscordIcon({ size }: OAuthIconsProps) {
|
|
54
52
|
const safeSize = getSafeSize(size);
|
|
55
53
|
return (
|
|
56
54
|
<svg
|
|
@@ -59,7 +57,6 @@ export function DiscordIcon({ size, className }: OAuthIconsProps) {
|
|
|
59
57
|
viewBox="0 0 24 24"
|
|
60
58
|
fill="none"
|
|
61
59
|
xmlns="http://www.w3.org/2000/svg"
|
|
62
|
-
className={className}
|
|
63
60
|
aria-hidden="true"
|
|
64
61
|
>
|
|
65
62
|
<path
|
|
@@ -73,18 +70,15 @@ export function DiscordIcon({ size, className }: OAuthIconsProps) {
|
|
|
73
70
|
/**
|
|
74
71
|
* Spinner Icon for loading state
|
|
75
72
|
*/
|
|
76
|
-
export function SpinnerIcon({ size
|
|
77
|
-
|
|
78
|
-
const safeSize = size ?? 20;
|
|
79
|
-
const finalSize = Math.max(1, safeSize);
|
|
73
|
+
export function SpinnerIcon({ size }: OAuthIconsProps) {
|
|
74
|
+
const safeSize = getSafeSize(size, 20);
|
|
80
75
|
return (
|
|
81
76
|
<svg
|
|
82
|
-
width={
|
|
83
|
-
height={
|
|
77
|
+
width={safeSize}
|
|
78
|
+
height={safeSize}
|
|
84
79
|
viewBox="0 0 24 24"
|
|
85
80
|
fill="none"
|
|
86
81
|
xmlns="http://www.w3.org/2000/svg"
|
|
87
|
-
className={className}
|
|
88
82
|
aria-hidden="true"
|
|
89
83
|
>
|
|
90
84
|
<circle
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { Story } from "@ladle/react";
|
|
2
|
+
import { useState, useCallback, useRef } from "react";
|
|
3
|
+
import { OAuthButton } from "./oauth-button";
|
|
4
|
+
import type { OAuthProvider } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to mimic real button behavior:
|
|
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]);
|
|
26
|
+
|
|
27
|
+
const onMouseLeave = useCallback(() => {
|
|
28
|
+
if (phase === "pressed") onMouseUp();
|
|
29
|
+
}, [phase, onMouseUp]);
|
|
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
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OAuthButton stories
|
|
40
|
+
*
|
|
41
|
+
* A headless OAuth button with sensible defaults built on base-ui.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
interface OAuthButtonStoryProps {
|
|
45
|
+
provider: OAuthProvider;
|
|
46
|
+
disabled: boolean;
|
|
47
|
+
loading: boolean;
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
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
|
+
export const Default: Story<OAuthButtonStoryProps> = ({
|
|
58
|
+
provider,
|
|
59
|
+
disabled,
|
|
60
|
+
loading,
|
|
61
|
+
label,
|
|
62
|
+
}) => {
|
|
63
|
+
const press = useRealPress();
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<OAuthButton
|
|
67
|
+
provider={provider}
|
|
68
|
+
disabled={disabled}
|
|
69
|
+
loading={loading}
|
|
70
|
+
onMouseDown={press.onMouseDown}
|
|
71
|
+
onMouseUp={press.onMouseUp}
|
|
72
|
+
onMouseLeave={press.onMouseLeave}
|
|
73
|
+
className={`${baseStyles} ${darkStyles} ${press.className}`}
|
|
74
|
+
>
|
|
75
|
+
{label || undefined}
|
|
76
|
+
</OAuthButton>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
Default.args = {
|
|
81
|
+
provider: "google",
|
|
82
|
+
disabled: false,
|
|
83
|
+
loading: false,
|
|
84
|
+
label: "",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
Default.argTypes = {
|
|
88
|
+
provider: {
|
|
89
|
+
control: { type: "select" },
|
|
90
|
+
options: ["google", "discord"],
|
|
91
|
+
},
|
|
92
|
+
label: {
|
|
93
|
+
control: { type: "text" },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Disabled: Story = () => (
|
|
98
|
+
<div className="flex flex-col gap-4">
|
|
99
|
+
<OAuthButton
|
|
100
|
+
provider="google"
|
|
101
|
+
disabled
|
|
102
|
+
className={`${baseStyles} opacity-60 cursor-not-allowed`}
|
|
103
|
+
>
|
|
104
|
+
Google (Disabled)
|
|
105
|
+
</OAuthButton>
|
|
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
|
+
);
|
|
126
|
+
|
|
127
|
+
export const LongLabel: Story = () => (
|
|
128
|
+
<div className="flex flex-col gap-4 max-w-md">
|
|
129
|
+
<OAuthButton provider="google" className={baseStyles}>
|
|
130
|
+
Sign in with your Google account to continue
|
|
131
|
+
</OAuthButton>
|
|
132
|
+
<OAuthButton provider="discord" className={baseStyles}>
|
|
133
|
+
Connect with Discord for community access and notifications
|
|
134
|
+
</OAuthButton>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
export const IconOnly: Story = () => {
|
|
139
|
+
const p1 = useRealPress();
|
|
140
|
+
const p2 = useRealPress();
|
|
141
|
+
|
|
142
|
+
return (
|
|
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>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const CircularIconOnly: Story = () => {
|
|
169
|
+
const g1 = useRealPress();
|
|
170
|
+
const d1 = useRealPress();
|
|
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>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const CustomStyling: Story = () => {
|
|
245
|
+
const g = useRealPress();
|
|
246
|
+
const d = useRealPress();
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div className="flex flex-col gap-4">
|
|
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>
|
|
268
|
+
</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";
|
|
277
|
+
|
|
278
|
+
export const AllStates: Story = () => {
|
|
279
|
+
const g = useRealPress();
|
|
280
|
+
const d = useRealPress();
|
|
281
|
+
const gCirc = useRealPress();
|
|
282
|
+
const dCirc = useRealPress();
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div className="grid grid-cols-3 gap-6 max-w-3xl">
|
|
286
|
+
<div className="space-y-2">
|
|
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>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
};
|
package/src/oauth-button.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { forwardRef
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
4
|
import { Button } from '@base-ui/react/button';
|
|
5
5
|
import type { OAuthButtonProps, OAuthProvider } from './types';
|
|
6
6
|
import { GoogleIcon, DiscordIcon, SpinnerIcon } from './icons';
|
|
@@ -13,19 +13,10 @@ const PROVIDER_NAMES: Record<OAuthProvider, string> = {
|
|
|
13
13
|
discord: 'Discord',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
switch (provider) {
|
|
21
|
-
case 'google':
|
|
22
|
-
return GoogleIcon;
|
|
23
|
-
case 'discord':
|
|
24
|
-
return DiscordIcon;
|
|
25
|
-
default:
|
|
26
|
-
return () => null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
16
|
+
const PROVIDER_ICONS: Record<OAuthProvider, typeof GoogleIcon> = {
|
|
17
|
+
google: GoogleIcon,
|
|
18
|
+
discord: DiscordIcon,
|
|
19
|
+
};
|
|
29
20
|
|
|
30
21
|
/**
|
|
31
22
|
* Sensible defaults that every button should have.
|
|
@@ -103,13 +94,9 @@ export const OAuthButton = forwardRef<HTMLButtonElement, OAuthButtonProps>(
|
|
|
103
94
|
} = props;
|
|
104
95
|
|
|
105
96
|
const isDisabled = disabled || loading;
|
|
106
|
-
const Icon =
|
|
97
|
+
const Icon = PROVIDER_ICONS[provider];
|
|
107
98
|
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
108
|
-
|
|
109
|
-
const mergedClassName = useMemo(
|
|
110
|
-
() => mergeButtonClasses(className, isDisabled),
|
|
111
|
-
[className, isDisabled]
|
|
112
|
-
);
|
|
99
|
+
const mergedClassName = mergeButtonClasses(className, isDisabled);
|
|
113
100
|
|
|
114
101
|
return (
|
|
115
102
|
<Button
|
|
@@ -136,4 +123,3 @@ export const OAuthButton = forwardRef<HTMLButtonElement, OAuthButtonProps>(
|
|
|
136
123
|
}
|
|
137
124
|
);
|
|
138
125
|
|
|
139
|
-
export default OAuthButton;
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
1
|
+
import type { ReactNode, ButtonHTMLAttributes } from 'react';
|
|
2
2
|
|
|
3
3
|
export type OAuthProvider = 'google' | 'discord';
|
|
4
4
|
|
|
5
|
-
export interface OAuthButtonProps {
|
|
5
|
+
export interface OAuthButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
|
6
6
|
/**
|
|
7
7
|
* The OAuth provider to authenticate with
|
|
8
8
|
*/
|
|
@@ -29,11 +29,6 @@ export interface OAuthButtonProps {
|
|
|
29
29
|
* Pass `null` for icon-only mode
|
|
30
30
|
*/
|
|
31
31
|
children?: ReactNode;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Disable the button
|
|
35
|
-
*/
|
|
36
|
-
disabled?: boolean;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
export interface OAuthIconsProps {
|
|
@@ -42,9 +37,4 @@ export interface OAuthIconsProps {
|
|
|
42
37
|
* @default 24
|
|
43
38
|
*/
|
|
44
39
|
size?: number;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Custom className
|
|
48
|
-
*/
|
|
49
|
-
className?: string;
|
|
50
40
|
}
|