@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 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, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
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, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
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, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
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, OAuthButton as default };
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, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
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, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
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, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
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, OAuthButton as default };
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
- const safeSize = size ?? 24;
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, className }) {
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, className }) {
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, className }) {
109
- const safeSize = size ?? 20;
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: finalSize,
115
- height: finalSize,
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
- function getProviderIcon(provider) {
168
- switch (provider) {
169
- case "google":
170
- return GoogleIcon;
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 = getProviderIcon(provider);
191
+ const Icon = PROVIDER_ICONS[provider];
204
192
  const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
205
- const mergedClassName = (0, import_react.useMemo)(
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, useMemo } from "react";
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
- const safeSize = size ?? 24;
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, className }) {
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, className }) {
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, className }) {
79
- const safeSize = size ?? 20;
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: finalSize,
85
- height: finalSize,
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
- function getProviderIcon(provider) {
138
- switch (provider) {
139
- case "google":
140
- return GoogleIcon;
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 = getProviderIcon(provider);
162
+ const Icon = PROVIDER_ICONS[provider];
174
163
  const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
175
- const mergedClassName = useMemo(
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublimee/auth-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Headless authentication UI components for Sublime",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
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
- const safeSize = size ?? 24;
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, className }: OAuthIconsProps) {
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, className }: OAuthIconsProps) {
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, className }: OAuthIconsProps) {
77
- // Spinner defaults to 20px instead of 24px
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={finalSize}
83
- height={finalSize}
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
@@ -20,7 +20,6 @@
20
20
 
21
21
  // Components
22
22
  export { OAuthButton } from './oauth-button';
23
- export { default } from './oauth-button';
24
23
 
25
24
  // Icons
26
25
  export { GoogleIcon, DiscordIcon, SpinnerIcon } from './icons';
@@ -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
+ };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { forwardRef, useMemo } from 'react';
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
- * Returns the appropriate icon component for the provider
18
- */
19
- function getProviderIcon(provider: OAuthProvider) {
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 = getProviderIcon(provider);
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
  }