@sublimee/auth-ui 0.1.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/index.d.mts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +236 -0
- package/dist/index.mjs +206 -0
- package/package.json +57 -0
- package/src/icons.tsx +118 -0
- package/src/index.ts +33 -0
- package/src/oauth-button.tsx +139 -0
- package/src/types.ts +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juan Figueroa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# @sublimee/auth-ui
|
|
2
|
+
|
|
3
|
+
Headless authentication UI components for Sublime, built on base-ui primitives.
|
|
4
|
+
|
|
5
|
+
## Design Philosophy
|
|
6
|
+
|
|
7
|
+
**"Headless but sensible"**
|
|
8
|
+
|
|
9
|
+
Our components are built on top of [base-ui](https://base-ui.com/) primitives. This means:
|
|
10
|
+
|
|
11
|
+
- ✅ **We inherit base-ui's sensible defaults** — cursor-pointer, focus states, keyboard navigation, accessibility
|
|
12
|
+
- ✅ **We DON'T impose visual opinions** — you control 100% of styling via Tailwind `className`
|
|
13
|
+
- ✅ **You get a solid, accessible foundation** that looks exactly how you want
|
|
14
|
+
|
|
15
|
+
This approach solves recurring UI issues (like missing `cursor: pointer`) once and for all, while preserving complete flexibility over visual design.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @sublimee/auth-ui
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Dependencies
|
|
24
|
+
|
|
25
|
+
- `@base-ui/react` — Button primitives with sensible defaults
|
|
26
|
+
- `react` ^19.0.0
|
|
27
|
+
- `react-dom` ^19.0.0
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
|
|
31
|
+
### OAuthButton
|
|
32
|
+
|
|
33
|
+
A headless OAuth authentication button with Google and Discord provider icons.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { OAuthButton } from '@sublimee/auth-ui';
|
|
37
|
+
|
|
38
|
+
// Basic usage — inherits cursor-pointer and focus states from base-ui
|
|
39
|
+
<OAuthButton
|
|
40
|
+
provider="google"
|
|
41
|
+
onClick={signInWithGoogle}
|
|
42
|
+
className="flex items-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg font-medium"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
// Outline style
|
|
46
|
+
<OAuthButton
|
|
47
|
+
provider="discord"
|
|
48
|
+
onClick={signInWithDiscord}
|
|
49
|
+
className="flex items-center gap-3 px-6 py-3 border border-white/20 text-white rounded-lg hover:bg-white/5"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
// Icon only — pass {null} as children
|
|
53
|
+
<OAuthButton
|
|
54
|
+
provider="google"
|
|
55
|
+
onClick={signInWithGoogle}
|
|
56
|
+
className="p-3 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
|
|
57
|
+
>
|
|
58
|
+
{null}
|
|
59
|
+
</OAuthButton>
|
|
60
|
+
|
|
61
|
+
// Loading state
|
|
62
|
+
<OAuthButton
|
|
63
|
+
provider="google"
|
|
64
|
+
loading
|
|
65
|
+
className="opacity-50"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
// Custom text
|
|
69
|
+
<OAuthButton
|
|
70
|
+
provider="google"
|
|
71
|
+
onClick={signInWithGoogle}
|
|
72
|
+
className="px-6 py-3 bg-blue-600 text-white rounded-lg"
|
|
73
|
+
>
|
|
74
|
+
Sign in with Google
|
|
75
|
+
</OAuthButton>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Props
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
|------|------|---------|-------------|
|
|
82
|
+
| `provider` | `'google' \| 'discord'` | required | OAuth provider to authenticate with |
|
|
83
|
+
| `onClick` | `() => void` | - | Click handler |
|
|
84
|
+
| `loading` | `boolean` | `false` | Shows spinner when true |
|
|
85
|
+
| `disabled` | `boolean` | `false` | Disables the button |
|
|
86
|
+
| `className` | `string` | - | Tailwind classes for styling |
|
|
87
|
+
| `children` | `ReactNode` | provider name | Button text. Pass `{null}` for icon-only |
|
|
88
|
+
|
|
89
|
+
#### Sensible defaults we provide
|
|
90
|
+
|
|
91
|
+
`OAuthButton` includes these UX fundamentals automatically:
|
|
92
|
+
|
|
93
|
+
| Behavior | Automatic? |
|
|
94
|
+
|----------|------------|
|
|
95
|
+
| `cursor: pointer` on hover | ✅ |
|
|
96
|
+
| `cursor: not-allowed` when disabled/loading | ✅ |
|
|
97
|
+
| **Focus visible states** via base-ui | ✅ |
|
|
98
|
+
| **Keyboard navigation** via base-ui | ✅ |
|
|
99
|
+
| **ARIA attributes** via base-ui | ✅ |
|
|
100
|
+
| **Disabled state handling** via base-ui | ✅ |
|
|
101
|
+
|
|
102
|
+
You don't need to add `cursor-pointer` to your className — it's automatic. Your `className` is merged with our sensible defaults, with your styles taking precedence.
|
|
103
|
+
|
|
104
|
+
### Icons
|
|
105
|
+
|
|
106
|
+
Individual icons are also exported if you need them separately:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { GoogleIcon, DiscordIcon, SpinnerIcon } from '@sublimee/auth-ui';
|
|
110
|
+
|
|
111
|
+
<GoogleIcon size={24} className="text-red-500" />
|
|
112
|
+
<DiscordIcon size={20} />
|
|
113
|
+
<SpinnerIcon size={16} className="animate-spin" />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Styling Guidelines
|
|
117
|
+
|
|
118
|
+
Since components are headless, you provide all styles via `className`. Here are recommended patterns:
|
|
119
|
+
|
|
120
|
+
### Common Patterns
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Full-width button
|
|
124
|
+
className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg"
|
|
125
|
+
|
|
126
|
+
// Minimal icon button
|
|
127
|
+
className="p-2 rounded-full hover:bg-white/10 transition-colors"
|
|
128
|
+
|
|
129
|
+
// Outline variant
|
|
130
|
+
className="flex items-center gap-2 px-4 py-2 border border-current rounded-md hover:bg-white/5"
|
|
131
|
+
|
|
132
|
+
// Loading state
|
|
133
|
+
className="opacity-50 cursor-wait"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### State Handling
|
|
137
|
+
|
|
138
|
+
The component accepts a `disabled` prop and renders in a loading state when `loading={true}`. These states automatically get `cursor: not-allowed` from base-ui, but you may want to add visual indicators:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<OAuthButton
|
|
142
|
+
provider="google"
|
|
143
|
+
loading={isLoading}
|
|
144
|
+
disabled={isLoading}
|
|
145
|
+
className={`flex items-center gap-3 px-6 py-3 rounded-lg ${
|
|
146
|
+
isLoading ? 'opacity-50' : 'hover:bg-gray-50'
|
|
147
|
+
}`}
|
|
148
|
+
/>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## TypeScript
|
|
152
|
+
|
|
153
|
+
Full TypeScript support is included:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import type { OAuthButtonProps, OAuthProvider, OAuthIconsProps } from '@sublimee/auth-ui';
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Why base-ui?
|
|
160
|
+
|
|
161
|
+
We chose base-ui as our foundation because:
|
|
162
|
+
|
|
163
|
+
1. **Sensible defaults out of the box** — No more forgetting `cursor: pointer`
|
|
164
|
+
2. **Accessibility built-in** — Proper ARIA, focus management, keyboard support
|
|
165
|
+
3. **Headless by design** — Complete control over styling
|
|
166
|
+
4. **Production-tested** — Built by the MUI team, battle-tested at scale
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
type OAuthProvider = 'google' | 'discord';
|
|
6
|
+
interface OAuthButtonProps {
|
|
7
|
+
/**
|
|
8
|
+
* The OAuth provider to authenticate with
|
|
9
|
+
*/
|
|
10
|
+
provider: OAuthProvider;
|
|
11
|
+
/**
|
|
12
|
+
* Click handler - typically calls supabase.auth.signInWithOAuth
|
|
13
|
+
*/
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Loading state - shows spinner when true
|
|
17
|
+
*/
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Custom className - use Tailwind for styling
|
|
21
|
+
* @example "flex items-center gap-2 px-4 py-2 bg-white text-gray-900 rounded-lg"
|
|
22
|
+
*/
|
|
23
|
+
className?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Button children - defaults to provider name
|
|
26
|
+
* Pass `null` for icon-only mode
|
|
27
|
+
*/
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Disable the button
|
|
31
|
+
*/
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface OAuthIconsProps {
|
|
35
|
+
/**
|
|
36
|
+
* Icon size in pixels (min: 1)
|
|
37
|
+
* @default 24
|
|
38
|
+
*/
|
|
39
|
+
size?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Custom className
|
|
42
|
+
*/
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* OAuthButton component
|
|
48
|
+
*
|
|
49
|
+
* A headless OAuth button built on top of base-ui's Button primitive.
|
|
50
|
+
* Includes sensible defaults (cursor-pointer) while remaining fully
|
|
51
|
+
* customizable via className.
|
|
52
|
+
*
|
|
53
|
+
* Design Philosophy:
|
|
54
|
+
* - We add sensible UX defaults (cursor-pointer, proper disabled states)
|
|
55
|
+
* - We don't impose visual opinions - you control all styling via className
|
|
56
|
+
* - The result is a solid, accessible foundation that looks exactly how you want
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* // Basic usage - cursor-pointer is automatic
|
|
61
|
+
* <OAuthButton
|
|
62
|
+
* provider="google"
|
|
63
|
+
* onClick={signInWithGoogle}
|
|
64
|
+
* className="flex items-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg font-medium"
|
|
65
|
+
* />
|
|
66
|
+
*
|
|
67
|
+
* // Icon only - pass {null} as children
|
|
68
|
+
* <OAuthButton
|
|
69
|
+
* provider="google"
|
|
70
|
+
* className="p-3 rounded-full hover:bg-white/10"
|
|
71
|
+
* >
|
|
72
|
+
* {null}
|
|
73
|
+
* </OAuthButton>
|
|
74
|
+
*
|
|
75
|
+
* // Loading state - cursor-not-allowed is automatic
|
|
76
|
+
* <OAuthButton provider="google" loading className="opacity-50" />
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare const OAuthButton: react.ForwardRefExoticComponent<OAuthButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Google Logo Icon
|
|
83
|
+
* Official Google "G" logo for OAuth authentication
|
|
84
|
+
*/
|
|
85
|
+
declare function GoogleIcon({ size, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
86
|
+
/**
|
|
87
|
+
* Discord Logo Icon
|
|
88
|
+
* Official Discord logo for OAuth authentication
|
|
89
|
+
*/
|
|
90
|
+
declare function DiscordIcon({ size, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
91
|
+
/**
|
|
92
|
+
* Spinner Icon for loading state
|
|
93
|
+
*/
|
|
94
|
+
declare function SpinnerIcon({ size, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
95
|
+
|
|
96
|
+
export { DiscordIcon, GoogleIcon, OAuthButton, type OAuthButtonProps, type OAuthIconsProps, type OAuthProvider, SpinnerIcon, OAuthButton as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
type OAuthProvider = 'google' | 'discord';
|
|
6
|
+
interface OAuthButtonProps {
|
|
7
|
+
/**
|
|
8
|
+
* The OAuth provider to authenticate with
|
|
9
|
+
*/
|
|
10
|
+
provider: OAuthProvider;
|
|
11
|
+
/**
|
|
12
|
+
* Click handler - typically calls supabase.auth.signInWithOAuth
|
|
13
|
+
*/
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Loading state - shows spinner when true
|
|
17
|
+
*/
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Custom className - use Tailwind for styling
|
|
21
|
+
* @example "flex items-center gap-2 px-4 py-2 bg-white text-gray-900 rounded-lg"
|
|
22
|
+
*/
|
|
23
|
+
className?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Button children - defaults to provider name
|
|
26
|
+
* Pass `null` for icon-only mode
|
|
27
|
+
*/
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Disable the button
|
|
31
|
+
*/
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface OAuthIconsProps {
|
|
35
|
+
/**
|
|
36
|
+
* Icon size in pixels (min: 1)
|
|
37
|
+
* @default 24
|
|
38
|
+
*/
|
|
39
|
+
size?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Custom className
|
|
42
|
+
*/
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* OAuthButton component
|
|
48
|
+
*
|
|
49
|
+
* A headless OAuth button built on top of base-ui's Button primitive.
|
|
50
|
+
* Includes sensible defaults (cursor-pointer) while remaining fully
|
|
51
|
+
* customizable via className.
|
|
52
|
+
*
|
|
53
|
+
* Design Philosophy:
|
|
54
|
+
* - We add sensible UX defaults (cursor-pointer, proper disabled states)
|
|
55
|
+
* - We don't impose visual opinions - you control all styling via className
|
|
56
|
+
* - The result is a solid, accessible foundation that looks exactly how you want
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* // Basic usage - cursor-pointer is automatic
|
|
61
|
+
* <OAuthButton
|
|
62
|
+
* provider="google"
|
|
63
|
+
* onClick={signInWithGoogle}
|
|
64
|
+
* className="flex items-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg font-medium"
|
|
65
|
+
* />
|
|
66
|
+
*
|
|
67
|
+
* // Icon only - pass {null} as children
|
|
68
|
+
* <OAuthButton
|
|
69
|
+
* provider="google"
|
|
70
|
+
* className="p-3 rounded-full hover:bg-white/10"
|
|
71
|
+
* >
|
|
72
|
+
* {null}
|
|
73
|
+
* </OAuthButton>
|
|
74
|
+
*
|
|
75
|
+
* // Loading state - cursor-not-allowed is automatic
|
|
76
|
+
* <OAuthButton provider="google" loading className="opacity-50" />
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare const OAuthButton: react.ForwardRefExoticComponent<OAuthButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Google Logo Icon
|
|
83
|
+
* Official Google "G" logo for OAuth authentication
|
|
84
|
+
*/
|
|
85
|
+
declare function GoogleIcon({ size, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
86
|
+
/**
|
|
87
|
+
* Discord Logo Icon
|
|
88
|
+
* Official Discord logo for OAuth authentication
|
|
89
|
+
*/
|
|
90
|
+
declare function DiscordIcon({ size, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
91
|
+
/**
|
|
92
|
+
* Spinner Icon for loading state
|
|
93
|
+
*/
|
|
94
|
+
declare function SpinnerIcon({ size, className }: OAuthIconsProps): react_jsx_runtime.JSX.Element;
|
|
95
|
+
|
|
96
|
+
export { DiscordIcon, GoogleIcon, OAuthButton, type OAuthButtonProps, type OAuthIconsProps, type OAuthProvider, SpinnerIcon, OAuthButton as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DiscordIcon: () => DiscordIcon,
|
|
24
|
+
GoogleIcon: () => GoogleIcon,
|
|
25
|
+
OAuthButton: () => OAuthButton,
|
|
26
|
+
SpinnerIcon: () => SpinnerIcon,
|
|
27
|
+
default: () => oauth_button_default
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/oauth-button.tsx
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
var import_button = require("@base-ui/react/button");
|
|
34
|
+
|
|
35
|
+
// src/icons.tsx
|
|
36
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
37
|
+
function getSafeSize(size) {
|
|
38
|
+
const safeSize = size ?? 24;
|
|
39
|
+
return Math.max(1, safeSize);
|
|
40
|
+
}
|
|
41
|
+
function GoogleIcon({ size, className }) {
|
|
42
|
+
const safeSize = getSafeSize(size);
|
|
43
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
44
|
+
"svg",
|
|
45
|
+
{
|
|
46
|
+
width: safeSize,
|
|
47
|
+
height: safeSize,
|
|
48
|
+
viewBox: "0 0 24 24",
|
|
49
|
+
fill: "none",
|
|
50
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
51
|
+
className,
|
|
52
|
+
"aria-hidden": "true",
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
55
|
+
"path",
|
|
56
|
+
{
|
|
57
|
+
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",
|
|
58
|
+
fill: "#4285F4"
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
62
|
+
"path",
|
|
63
|
+
{
|
|
64
|
+
d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z",
|
|
65
|
+
fill: "#34A853"
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
69
|
+
"path",
|
|
70
|
+
{
|
|
71
|
+
d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z",
|
|
72
|
+
fill: "#FBBC05"
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
76
|
+
"path",
|
|
77
|
+
{
|
|
78
|
+
d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z",
|
|
79
|
+
fill: "#EA4335"
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
function DiscordIcon({ size, className }) {
|
|
87
|
+
const safeSize = getSafeSize(size);
|
|
88
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
89
|
+
"svg",
|
|
90
|
+
{
|
|
91
|
+
width: safeSize,
|
|
92
|
+
height: safeSize,
|
|
93
|
+
viewBox: "0 0 24 24",
|
|
94
|
+
fill: "none",
|
|
95
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
96
|
+
className,
|
|
97
|
+
"aria-hidden": "true",
|
|
98
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
|
+
"path",
|
|
100
|
+
{
|
|
101
|
+
d: "M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z",
|
|
102
|
+
fill: "currentColor"
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function SpinnerIcon({ size, className }) {
|
|
109
|
+
const safeSize = size ?? 20;
|
|
110
|
+
const finalSize = Math.max(1, safeSize);
|
|
111
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
112
|
+
"svg",
|
|
113
|
+
{
|
|
114
|
+
width: finalSize,
|
|
115
|
+
height: finalSize,
|
|
116
|
+
viewBox: "0 0 24 24",
|
|
117
|
+
fill: "none",
|
|
118
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
119
|
+
className,
|
|
120
|
+
"aria-hidden": "true",
|
|
121
|
+
children: [
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
123
|
+
"circle",
|
|
124
|
+
{
|
|
125
|
+
cx: "12",
|
|
126
|
+
cy: "12",
|
|
127
|
+
r: "10",
|
|
128
|
+
stroke: "currentColor",
|
|
129
|
+
strokeWidth: "4",
|
|
130
|
+
strokeLinecap: "round",
|
|
131
|
+
strokeDasharray: "60",
|
|
132
|
+
strokeDashoffset: "20",
|
|
133
|
+
opacity: "0.25"
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
137
|
+
"path",
|
|
138
|
+
{
|
|
139
|
+
d: "M12 2C6.477 2 2 6.477 2 12",
|
|
140
|
+
stroke: "currentColor",
|
|
141
|
+
strokeWidth: "4",
|
|
142
|
+
strokeLinecap: "round",
|
|
143
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
144
|
+
"animateTransform",
|
|
145
|
+
{
|
|
146
|
+
attributeName: "transform",
|
|
147
|
+
type: "rotate",
|
|
148
|
+
from: "0 12 12",
|
|
149
|
+
to: "360 12 12",
|
|
150
|
+
dur: "1s",
|
|
151
|
+
repeatCount: "indefinite"
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/oauth-button.tsx
|
|
162
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
163
|
+
var PROVIDER_NAMES = {
|
|
164
|
+
google: "Google",
|
|
165
|
+
discord: "Discord"
|
|
166
|
+
};
|
|
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
|
+
}
|
|
177
|
+
var SENSIBLE_DEFAULTS = ["cursor-pointer"];
|
|
178
|
+
function mergeButtonClasses(userClassName, isDisabled) {
|
|
179
|
+
const userClasses = userClassName?.trim() ?? "";
|
|
180
|
+
const neededDefaults = SENSIBLE_DEFAULTS.filter(
|
|
181
|
+
(defaultClass) => !userClasses.includes(defaultClass)
|
|
182
|
+
);
|
|
183
|
+
if (isDisabled && !userClasses.includes("cursor-not-allowed")) {
|
|
184
|
+
neededDefaults.push("cursor-not-allowed");
|
|
185
|
+
}
|
|
186
|
+
if (neededDefaults.length === 0) {
|
|
187
|
+
return userClasses;
|
|
188
|
+
}
|
|
189
|
+
return `${neededDefaults.join(" ")} ${userClasses}`.trim();
|
|
190
|
+
}
|
|
191
|
+
var OAuthButton = (0, import_react.forwardRef)(
|
|
192
|
+
function OAuthButton2(props, forwardedRef) {
|
|
193
|
+
const {
|
|
194
|
+
provider,
|
|
195
|
+
onClick,
|
|
196
|
+
loading = false,
|
|
197
|
+
className,
|
|
198
|
+
children,
|
|
199
|
+
disabled = false,
|
|
200
|
+
...otherProps
|
|
201
|
+
} = props;
|
|
202
|
+
const isDisabled = disabled || loading;
|
|
203
|
+
const Icon = getProviderIcon(provider);
|
|
204
|
+
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
205
|
+
const mergedClassName = (0, import_react.useMemo)(
|
|
206
|
+
() => mergeButtonClasses(className, isDisabled),
|
|
207
|
+
[className, isDisabled]
|
|
208
|
+
);
|
|
209
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
210
|
+
import_button.Button,
|
|
211
|
+
{
|
|
212
|
+
ref: forwardedRef,
|
|
213
|
+
onClick,
|
|
214
|
+
disabled: isDisabled,
|
|
215
|
+
"aria-busy": loading,
|
|
216
|
+
className: mergedClassName,
|
|
217
|
+
...otherProps,
|
|
218
|
+
children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SpinnerIcon, {}),
|
|
220
|
+
children !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: children ?? "Conectando..." })
|
|
221
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, {}),
|
|
223
|
+
children !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: children ?? defaultText })
|
|
224
|
+
] })
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
var oauth_button_default = OAuthButton;
|
|
230
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
231
|
+
0 && (module.exports = {
|
|
232
|
+
DiscordIcon,
|
|
233
|
+
GoogleIcon,
|
|
234
|
+
OAuthButton,
|
|
235
|
+
SpinnerIcon
|
|
236
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/oauth-button.tsx
|
|
2
|
+
import { forwardRef, useMemo } from "react";
|
|
3
|
+
import { Button } from "@base-ui/react/button";
|
|
4
|
+
|
|
5
|
+
// src/icons.tsx
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function getSafeSize(size) {
|
|
8
|
+
const safeSize = size ?? 24;
|
|
9
|
+
return Math.max(1, safeSize);
|
|
10
|
+
}
|
|
11
|
+
function GoogleIcon({ size, className }) {
|
|
12
|
+
const safeSize = getSafeSize(size);
|
|
13
|
+
return /* @__PURE__ */ jsxs(
|
|
14
|
+
"svg",
|
|
15
|
+
{
|
|
16
|
+
width: safeSize,
|
|
17
|
+
height: safeSize,
|
|
18
|
+
viewBox: "0 0 24 24",
|
|
19
|
+
fill: "none",
|
|
20
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
21
|
+
className,
|
|
22
|
+
"aria-hidden": "true",
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsx(
|
|
25
|
+
"path",
|
|
26
|
+
{
|
|
27
|
+
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",
|
|
28
|
+
fill: "#4285F4"
|
|
29
|
+
}
|
|
30
|
+
),
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
"path",
|
|
33
|
+
{
|
|
34
|
+
d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z",
|
|
35
|
+
fill: "#34A853"
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
/* @__PURE__ */ jsx(
|
|
39
|
+
"path",
|
|
40
|
+
{
|
|
41
|
+
d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z",
|
|
42
|
+
fill: "#FBBC05"
|
|
43
|
+
}
|
|
44
|
+
),
|
|
45
|
+
/* @__PURE__ */ jsx(
|
|
46
|
+
"path",
|
|
47
|
+
{
|
|
48
|
+
d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z",
|
|
49
|
+
fill: "#EA4335"
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function DiscordIcon({ size, className }) {
|
|
57
|
+
const safeSize = getSafeSize(size);
|
|
58
|
+
return /* @__PURE__ */ jsx(
|
|
59
|
+
"svg",
|
|
60
|
+
{
|
|
61
|
+
width: safeSize,
|
|
62
|
+
height: safeSize,
|
|
63
|
+
viewBox: "0 0 24 24",
|
|
64
|
+
fill: "none",
|
|
65
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
66
|
+
className,
|
|
67
|
+
"aria-hidden": "true",
|
|
68
|
+
children: /* @__PURE__ */ jsx(
|
|
69
|
+
"path",
|
|
70
|
+
{
|
|
71
|
+
d: "M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z",
|
|
72
|
+
fill: "currentColor"
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
function SpinnerIcon({ size, className }) {
|
|
79
|
+
const safeSize = size ?? 20;
|
|
80
|
+
const finalSize = Math.max(1, safeSize);
|
|
81
|
+
return /* @__PURE__ */ jsxs(
|
|
82
|
+
"svg",
|
|
83
|
+
{
|
|
84
|
+
width: finalSize,
|
|
85
|
+
height: finalSize,
|
|
86
|
+
viewBox: "0 0 24 24",
|
|
87
|
+
fill: "none",
|
|
88
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
89
|
+
className,
|
|
90
|
+
"aria-hidden": "true",
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
"circle",
|
|
94
|
+
{
|
|
95
|
+
cx: "12",
|
|
96
|
+
cy: "12",
|
|
97
|
+
r: "10",
|
|
98
|
+
stroke: "currentColor",
|
|
99
|
+
strokeWidth: "4",
|
|
100
|
+
strokeLinecap: "round",
|
|
101
|
+
strokeDasharray: "60",
|
|
102
|
+
strokeDashoffset: "20",
|
|
103
|
+
opacity: "0.25"
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsx(
|
|
107
|
+
"path",
|
|
108
|
+
{
|
|
109
|
+
d: "M12 2C6.477 2 2 6.477 2 12",
|
|
110
|
+
stroke: "currentColor",
|
|
111
|
+
strokeWidth: "4",
|
|
112
|
+
strokeLinecap: "round",
|
|
113
|
+
children: /* @__PURE__ */ jsx(
|
|
114
|
+
"animateTransform",
|
|
115
|
+
{
|
|
116
|
+
attributeName: "transform",
|
|
117
|
+
type: "rotate",
|
|
118
|
+
from: "0 12 12",
|
|
119
|
+
to: "360 12 12",
|
|
120
|
+
dur: "1s",
|
|
121
|
+
repeatCount: "indefinite"
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/oauth-button.tsx
|
|
132
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
133
|
+
var PROVIDER_NAMES = {
|
|
134
|
+
google: "Google",
|
|
135
|
+
discord: "Discord"
|
|
136
|
+
};
|
|
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
|
+
}
|
|
147
|
+
var SENSIBLE_DEFAULTS = ["cursor-pointer"];
|
|
148
|
+
function mergeButtonClasses(userClassName, isDisabled) {
|
|
149
|
+
const userClasses = userClassName?.trim() ?? "";
|
|
150
|
+
const neededDefaults = SENSIBLE_DEFAULTS.filter(
|
|
151
|
+
(defaultClass) => !userClasses.includes(defaultClass)
|
|
152
|
+
);
|
|
153
|
+
if (isDisabled && !userClasses.includes("cursor-not-allowed")) {
|
|
154
|
+
neededDefaults.push("cursor-not-allowed");
|
|
155
|
+
}
|
|
156
|
+
if (neededDefaults.length === 0) {
|
|
157
|
+
return userClasses;
|
|
158
|
+
}
|
|
159
|
+
return `${neededDefaults.join(" ")} ${userClasses}`.trim();
|
|
160
|
+
}
|
|
161
|
+
var OAuthButton = forwardRef(
|
|
162
|
+
function OAuthButton2(props, forwardedRef) {
|
|
163
|
+
const {
|
|
164
|
+
provider,
|
|
165
|
+
onClick,
|
|
166
|
+
loading = false,
|
|
167
|
+
className,
|
|
168
|
+
children,
|
|
169
|
+
disabled = false,
|
|
170
|
+
...otherProps
|
|
171
|
+
} = props;
|
|
172
|
+
const isDisabled = disabled || loading;
|
|
173
|
+
const Icon = getProviderIcon(provider);
|
|
174
|
+
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
175
|
+
const mergedClassName = useMemo(
|
|
176
|
+
() => mergeButtonClasses(className, isDisabled),
|
|
177
|
+
[className, isDisabled]
|
|
178
|
+
);
|
|
179
|
+
return /* @__PURE__ */ jsx2(
|
|
180
|
+
Button,
|
|
181
|
+
{
|
|
182
|
+
ref: forwardedRef,
|
|
183
|
+
onClick,
|
|
184
|
+
disabled: isDisabled,
|
|
185
|
+
"aria-busy": loading,
|
|
186
|
+
className: mergedClassName,
|
|
187
|
+
...otherProps,
|
|
188
|
+
children: loading ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
189
|
+
/* @__PURE__ */ jsx2(SpinnerIcon, {}),
|
|
190
|
+
children !== null && /* @__PURE__ */ jsx2("span", { children: children ?? "Conectando..." })
|
|
191
|
+
] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
192
|
+
/* @__PURE__ */ jsx2(Icon, {}),
|
|
193
|
+
children !== null && /* @__PURE__ */ jsx2("span", { children: children ?? defaultText })
|
|
194
|
+
] })
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
var oauth_button_default = OAuthButton;
|
|
200
|
+
export {
|
|
201
|
+
DiscordIcon,
|
|
202
|
+
GoogleIcon,
|
|
203
|
+
OAuthButton,
|
|
204
|
+
SpinnerIcon,
|
|
205
|
+
oauth_button_default as default
|
|
206
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sublimee/auth-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless authentication UI components for Sublime",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/juancrfig/sublime.git",
|
|
26
|
+
"directory": "packages/auth-ui"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"react",
|
|
30
|
+
"authentication",
|
|
31
|
+
"oauth",
|
|
32
|
+
"ui",
|
|
33
|
+
"components",
|
|
34
|
+
"headless",
|
|
35
|
+
"base-ui"
|
|
36
|
+
],
|
|
37
|
+
"author": "Sublime Team",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^19.0.0",
|
|
41
|
+
"react-dom": "^19.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@base-ui/react": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/react": "^19",
|
|
48
|
+
"@types/react-dom": "^19",
|
|
49
|
+
"tsup": "^8",
|
|
50
|
+
"typescript": "^5"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
54
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/icons.tsx
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { OAuthIconsProps } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates and sanitizes icon size (minimum 1px)
|
|
7
|
+
*/
|
|
8
|
+
function getSafeSize(size: number | undefined): number {
|
|
9
|
+
const safeSize = size ?? 24;
|
|
10
|
+
return Math.max(1, safeSize);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Google Logo Icon
|
|
15
|
+
* Official Google "G" logo for OAuth authentication
|
|
16
|
+
*/
|
|
17
|
+
export function GoogleIcon({ size, className }: OAuthIconsProps) {
|
|
18
|
+
const safeSize = getSafeSize(size);
|
|
19
|
+
return (
|
|
20
|
+
<svg
|
|
21
|
+
width={safeSize}
|
|
22
|
+
height={safeSize}
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
fill="none"
|
|
25
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
+
className={className}
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
>
|
|
29
|
+
<path
|
|
30
|
+
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
|
+
fill="#4285F4"
|
|
32
|
+
/>
|
|
33
|
+
<path
|
|
34
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
35
|
+
fill="#34A853"
|
|
36
|
+
/>
|
|
37
|
+
<path
|
|
38
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
39
|
+
fill="#FBBC05"
|
|
40
|
+
/>
|
|
41
|
+
<path
|
|
42
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
43
|
+
fill="#EA4335"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Discord Logo Icon
|
|
51
|
+
* Official Discord logo for OAuth authentication
|
|
52
|
+
*/
|
|
53
|
+
export function DiscordIcon({ size, className }: OAuthIconsProps) {
|
|
54
|
+
const safeSize = getSafeSize(size);
|
|
55
|
+
return (
|
|
56
|
+
<svg
|
|
57
|
+
width={safeSize}
|
|
58
|
+
height={safeSize}
|
|
59
|
+
viewBox="0 0 24 24"
|
|
60
|
+
fill="none"
|
|
61
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
62
|
+
className={className}
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
>
|
|
65
|
+
<path
|
|
66
|
+
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
|
|
67
|
+
fill="currentColor"
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Spinner Icon for loading state
|
|
75
|
+
*/
|
|
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);
|
|
80
|
+
return (
|
|
81
|
+
<svg
|
|
82
|
+
width={finalSize}
|
|
83
|
+
height={finalSize}
|
|
84
|
+
viewBox="0 0 24 24"
|
|
85
|
+
fill="none"
|
|
86
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
87
|
+
className={className}
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
>
|
|
90
|
+
<circle
|
|
91
|
+
cx="12"
|
|
92
|
+
cy="12"
|
|
93
|
+
r="10"
|
|
94
|
+
stroke="currentColor"
|
|
95
|
+
strokeWidth="4"
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeDasharray="60"
|
|
98
|
+
strokeDashoffset="20"
|
|
99
|
+
opacity="0.25"
|
|
100
|
+
/>
|
|
101
|
+
<path
|
|
102
|
+
d="M12 2C6.477 2 2 6.477 2 12"
|
|
103
|
+
stroke="currentColor"
|
|
104
|
+
strokeWidth="4"
|
|
105
|
+
strokeLinecap="round"
|
|
106
|
+
>
|
|
107
|
+
<animateTransform
|
|
108
|
+
attributeName="transform"
|
|
109
|
+
type="rotate"
|
|
110
|
+
from="0 12 12"
|
|
111
|
+
to="360 12 12"
|
|
112
|
+
dur="1s"
|
|
113
|
+
repeatCount="indefinite"
|
|
114
|
+
/>
|
|
115
|
+
</path>
|
|
116
|
+
</svg>
|
|
117
|
+
);
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sublimee/auth-ui
|
|
3
|
+
* Headless authentication UI components for Sublime
|
|
4
|
+
*
|
|
5
|
+
* Design Philosophy:
|
|
6
|
+
* -----------------
|
|
7
|
+
* "Headless but sensible"
|
|
8
|
+
*
|
|
9
|
+
* Our components are built on top of base-ui primitives, which means:
|
|
10
|
+
* - We inherit base-ui's accessibility defaults (focus states, keyboard nav, ARIA)
|
|
11
|
+
* - We ADD sensible UX defaults (cursor-pointer, disabled states)
|
|
12
|
+
* - We DON'T impose visual opinions - you control 100% of styling via className
|
|
13
|
+
* - You get a solid, accessible foundation that looks exactly how you want
|
|
14
|
+
*
|
|
15
|
+
* This approach solves recurring UI issues (like cursor:pointer) once and for all,
|
|
16
|
+
* while preserving the flexibility to style however you need.
|
|
17
|
+
*
|
|
18
|
+
* Built with base-ui for maximum flexibility and composability.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Components
|
|
22
|
+
export { OAuthButton } from './oauth-button';
|
|
23
|
+
export { default } from './oauth-button';
|
|
24
|
+
|
|
25
|
+
// Icons
|
|
26
|
+
export { GoogleIcon, DiscordIcon, SpinnerIcon } from './icons';
|
|
27
|
+
|
|
28
|
+
// Types
|
|
29
|
+
export type {
|
|
30
|
+
OAuthProvider,
|
|
31
|
+
OAuthButtonProps,
|
|
32
|
+
OAuthIconsProps,
|
|
33
|
+
} from './types';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useMemo } from 'react';
|
|
4
|
+
import { Button } from '@base-ui/react/button';
|
|
5
|
+
import type { OAuthButtonProps, OAuthProvider } from './types';
|
|
6
|
+
import { GoogleIcon, DiscordIcon, SpinnerIcon } from './icons';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Maps OAuth providers to their display names
|
|
10
|
+
*/
|
|
11
|
+
const PROVIDER_NAMES: Record<OAuthProvider, string> = {
|
|
12
|
+
google: 'Google',
|
|
13
|
+
discord: 'Discord',
|
|
14
|
+
};
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sensible defaults that every button should have.
|
|
32
|
+
* These are UX fundamentals that don't affect visual design.
|
|
33
|
+
*/
|
|
34
|
+
const SENSIBLE_DEFAULTS: readonly string[] = ['cursor-pointer'];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Merges sensible default classes with user-provided classes.
|
|
38
|
+
* Prevents duplicate cursor classes if user already specified them.
|
|
39
|
+
*/
|
|
40
|
+
function mergeButtonClasses(userClassName: string | undefined, isDisabled: boolean): string {
|
|
41
|
+
const userClasses = userClassName?.trim() ?? '';
|
|
42
|
+
|
|
43
|
+
// Filter out defaults that user already specified to avoid duplicates
|
|
44
|
+
const neededDefaults = SENSIBLE_DEFAULTS.filter(
|
|
45
|
+
defaultClass => !userClasses.includes(defaultClass)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Add disabled cursor class if needed and not already present
|
|
49
|
+
if (isDisabled && !userClasses.includes('cursor-not-allowed')) {
|
|
50
|
+
neededDefaults.push('cursor-not-allowed');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (neededDefaults.length === 0) {
|
|
54
|
+
return userClasses;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${neededDefaults.join(' ')} ${userClasses}`.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* OAuthButton component
|
|
62
|
+
*
|
|
63
|
+
* A headless OAuth button built on top of base-ui's Button primitive.
|
|
64
|
+
* Includes sensible defaults (cursor-pointer) while remaining fully
|
|
65
|
+
* customizable via className.
|
|
66
|
+
*
|
|
67
|
+
* Design Philosophy:
|
|
68
|
+
* - We add sensible UX defaults (cursor-pointer, proper disabled states)
|
|
69
|
+
* - We don't impose visual opinions - you control all styling via className
|
|
70
|
+
* - The result is a solid, accessible foundation that looks exactly how you want
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* // Basic usage - cursor-pointer is automatic
|
|
75
|
+
* <OAuthButton
|
|
76
|
+
* provider="google"
|
|
77
|
+
* onClick={signInWithGoogle}
|
|
78
|
+
* className="flex items-center gap-3 px-6 py-3 bg-white text-gray-900 rounded-lg font-medium"
|
|
79
|
+
* />
|
|
80
|
+
*
|
|
81
|
+
* // Icon only - pass {null} as children
|
|
82
|
+
* <OAuthButton
|
|
83
|
+
* provider="google"
|
|
84
|
+
* className="p-3 rounded-full hover:bg-white/10"
|
|
85
|
+
* >
|
|
86
|
+
* {null}
|
|
87
|
+
* </OAuthButton>
|
|
88
|
+
*
|
|
89
|
+
* // Loading state - cursor-not-allowed is automatic
|
|
90
|
+
* <OAuthButton provider="google" loading className="opacity-50" />
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export const OAuthButton = forwardRef<HTMLButtonElement, OAuthButtonProps>(
|
|
94
|
+
function OAuthButton(props, forwardedRef) {
|
|
95
|
+
const {
|
|
96
|
+
provider,
|
|
97
|
+
onClick,
|
|
98
|
+
loading = false,
|
|
99
|
+
className,
|
|
100
|
+
children,
|
|
101
|
+
disabled = false,
|
|
102
|
+
...otherProps
|
|
103
|
+
} = props;
|
|
104
|
+
|
|
105
|
+
const isDisabled = disabled || loading;
|
|
106
|
+
const Icon = getProviderIcon(provider);
|
|
107
|
+
const defaultText = `Continuar con ${PROVIDER_NAMES[provider]}`;
|
|
108
|
+
|
|
109
|
+
const mergedClassName = useMemo(
|
|
110
|
+
() => mergeButtonClasses(className, isDisabled),
|
|
111
|
+
[className, isDisabled]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Button
|
|
116
|
+
ref={forwardedRef}
|
|
117
|
+
onClick={onClick}
|
|
118
|
+
disabled={isDisabled}
|
|
119
|
+
aria-busy={loading}
|
|
120
|
+
className={mergedClassName}
|
|
121
|
+
{...otherProps}
|
|
122
|
+
>
|
|
123
|
+
{loading ? (
|
|
124
|
+
<>
|
|
125
|
+
<SpinnerIcon />
|
|
126
|
+
{children !== null && <span>{children ?? 'Conectando...'}</span>}
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
<>
|
|
130
|
+
<Icon />
|
|
131
|
+
{children !== null && <span>{children ?? defaultText}</span>}
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</Button>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
export default OAuthButton;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type OAuthProvider = 'google' | 'discord';
|
|
4
|
+
|
|
5
|
+
export interface OAuthButtonProps {
|
|
6
|
+
/**
|
|
7
|
+
* The OAuth provider to authenticate with
|
|
8
|
+
*/
|
|
9
|
+
provider: OAuthProvider;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Click handler - typically calls supabase.auth.signInWithOAuth
|
|
13
|
+
*/
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loading state - shows spinner when true
|
|
18
|
+
*/
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Custom className - use Tailwind for styling
|
|
23
|
+
* @example "flex items-center gap-2 px-4 py-2 bg-white text-gray-900 rounded-lg"
|
|
24
|
+
*/
|
|
25
|
+
className?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Button children - defaults to provider name
|
|
29
|
+
* Pass `null` for icon-only mode
|
|
30
|
+
*/
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Disable the button
|
|
35
|
+
*/
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OAuthIconsProps {
|
|
40
|
+
/**
|
|
41
|
+
* Icon size in pixels (min: 1)
|
|
42
|
+
* @default 24
|
|
43
|
+
*/
|
|
44
|
+
size?: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Custom className
|
|
48
|
+
*/
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|