@sublimee/auth-ui 0.1.0 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_INDEX.md +64 -0
- package/README.md +80 -120
- package/dist/animations.css +52 -0
- package/dist/index.d.mts +55 -46
- package/dist/index.d.ts +55 -46
- package/dist/index.js +449 -78
- package/dist/index.mjs +449 -78
- package/package.json +9 -6
- package/src/animations.css +52 -0
- package/src/base-button.stories.tsx +96 -0
- package/src/base-button.tsx +124 -0
- package/src/button-loading-indicator.tsx +33 -0
- package/src/icons.tsx +20 -33
- package/src/index.ts +25 -20
- package/src/oauth-button.stories.tsx +124 -0
- package/src/oauth-button.tsx +40 -99
- package/src/runtime-styles.ts +225 -0
- package/src/types.ts +62 -16
- package/src/use-button-animation.ts +114 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sublimee/auth-ui runtime animations
|
|
3
|
+
*
|
|
4
|
+
* Optional legacy export.
|
|
5
|
+
* Most consumers should not need to import this file because auth-ui now
|
|
6
|
+
* injects its runtime button styles automatically.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
@keyframes sublime-button-press-release {
|
|
10
|
+
0% {
|
|
11
|
+
transform: translateY(1px) scale(0.985);
|
|
12
|
+
box-shadow: var(--sublime-button-shadow-active);
|
|
13
|
+
}
|
|
14
|
+
100% {
|
|
15
|
+
transform: translateY(0) scale(1);
|
|
16
|
+
box-shadow: var(--sublime-button-shadow);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@keyframes sublime-button-spinner {
|
|
21
|
+
from {
|
|
22
|
+
transform: rotate(0deg);
|
|
23
|
+
}
|
|
24
|
+
to {
|
|
25
|
+
transform: rotate(360deg);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sublime-button-pressed {
|
|
30
|
+
transform: translateY(1px) scale(0.985);
|
|
31
|
+
box-shadow: var(--sublime-button-shadow-active);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.animate-sublime-button-press-release {
|
|
35
|
+
animation: sublime-button-press-release 180ms var(--sublime-ease-out, cubic-bezier(0, 0, 0.2, 1));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.sublime-button__spinner {
|
|
39
|
+
animation: sublime-button-spinner 720ms linear infinite;
|
|
40
|
+
transform-origin: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@media (prefers-reduced-motion: reduce) {
|
|
44
|
+
.animate-sublime-button-press-release {
|
|
45
|
+
animation-duration: 0.01ms !important;
|
|
46
|
+
animation-iteration-count: 1 !important;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sublime-button__spinner {
|
|
50
|
+
animation-duration: 1.6s !important;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Story } from '@ladle/react';
|
|
2
|
+
|
|
3
|
+
import { BaseButton } from './base-button';
|
|
4
|
+
import type { ButtonVariant } from './types';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'A Base Button',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface BaseButtonStoryProps {
|
|
11
|
+
children: string;
|
|
12
|
+
disabled: boolean;
|
|
13
|
+
isIconOnly: boolean;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
variant: ButtonVariant;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Default: Story<BaseButtonStoryProps> = ({
|
|
19
|
+
children,
|
|
20
|
+
disabled,
|
|
21
|
+
isIconOnly,
|
|
22
|
+
loading,
|
|
23
|
+
variant,
|
|
24
|
+
}) => (
|
|
25
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
26
|
+
<BaseButton
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
isIconOnly={isIconOnly}
|
|
29
|
+
loading={loading}
|
|
30
|
+
variant={variant}
|
|
31
|
+
aria-label={isIconOnly ? children : undefined}
|
|
32
|
+
>
|
|
33
|
+
{isIconOnly ? <span aria-hidden="true">G</span> : children}
|
|
34
|
+
</BaseButton>
|
|
35
|
+
|
|
36
|
+
<BaseButton
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
loading={loading}
|
|
39
|
+
isIconOnly
|
|
40
|
+
variant="secondary"
|
|
41
|
+
aria-label="Quick action"
|
|
42
|
+
>
|
|
43
|
+
<span aria-hidden="true">+</span>
|
|
44
|
+
</BaseButton>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
Default.args = {
|
|
49
|
+
children: 'Continue',
|
|
50
|
+
disabled: false,
|
|
51
|
+
isIconOnly: false,
|
|
52
|
+
loading: false,
|
|
53
|
+
variant: 'secondary',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
Default.argTypes = {
|
|
57
|
+
variant: {
|
|
58
|
+
control: { type: 'select' },
|
|
59
|
+
options: ['primary', 'secondary'],
|
|
60
|
+
},
|
|
61
|
+
children: {
|
|
62
|
+
control: { type: 'text' },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const AllVariants: Story = () => (
|
|
67
|
+
<div className="flex flex-col gap-4 items-start">
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Primary</p>
|
|
70
|
+
<BaseButton variant="primary">Continue with email</BaseButton>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="space-y-2">
|
|
74
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Secondary</p>
|
|
75
|
+
<BaseButton variant="secondary">Continue with email</BaseButton>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const States: Story = () => (
|
|
81
|
+
<div className="flex flex-col gap-4 items-start">
|
|
82
|
+
<BaseButton>Default</BaseButton>
|
|
83
|
+
<BaseButton disabled>Disabled</BaseButton>
|
|
84
|
+
<BaseButton loading>Saving changes</BaseButton>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const DarkMode: Story = () => (
|
|
89
|
+
<div className="dark bg-[var(--sublime-color-surface-0)] p-8 rounded-lg">
|
|
90
|
+
<div className="flex flex-col gap-4 items-start">
|
|
91
|
+
<BaseButton variant="primary">Primary</BaseButton>
|
|
92
|
+
<BaseButton variant="secondary">Secondary</BaseButton>
|
|
93
|
+
<BaseButton loading variant="secondary">Loading</BaseButton>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { Button } from '@base-ui/react/button';
|
|
5
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
import { ButtonLoadingIndicator } from './button-loading-indicator';
|
|
8
|
+
import { useButtonAnimation } from './use-button-animation';
|
|
9
|
+
import { useAuthUiRuntimeStyles } from './runtime-styles';
|
|
10
|
+
import type {
|
|
11
|
+
ButtonAnimation,
|
|
12
|
+
ButtonLoadingAnimation,
|
|
13
|
+
ButtonVariant,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
interface BaseButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
|
17
|
+
animation?: ButtonAnimation;
|
|
18
|
+
isIconOnly?: boolean;
|
|
19
|
+
leadingVisual?: ReactNode;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
loadingAnimation?: ButtonLoadingAnimation;
|
|
22
|
+
onClick?: ButtonHTMLAttributes<HTMLButtonElement>['onClick'];
|
|
23
|
+
variant?: ButtonVariant;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type EventHandler<Event> = ((event: Event) => void) | undefined;
|
|
27
|
+
|
|
28
|
+
function mergeClassNames(...classNames: Array<string | undefined>): string {
|
|
29
|
+
return classNames.filter(Boolean).join(' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function composeEventHandlers<Event>(
|
|
33
|
+
internalHandler: EventHandler<Event>,
|
|
34
|
+
externalHandler: EventHandler<Event>
|
|
35
|
+
): EventHandler<Event> {
|
|
36
|
+
if (!internalHandler) {
|
|
37
|
+
return externalHandler;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!externalHandler) {
|
|
41
|
+
return internalHandler;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (event: Event) => {
|
|
45
|
+
internalHandler(event);
|
|
46
|
+
externalHandler(event);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
|
|
51
|
+
function BaseButton(props, forwardedRef) {
|
|
52
|
+
const {
|
|
53
|
+
animation = 'press',
|
|
54
|
+
'aria-busy': ariaBusy,
|
|
55
|
+
children,
|
|
56
|
+
className,
|
|
57
|
+
disabled = false,
|
|
58
|
+
isIconOnly = false,
|
|
59
|
+
leadingVisual,
|
|
60
|
+
loading = false,
|
|
61
|
+
loadingAnimation = 'spinner',
|
|
62
|
+
type = 'button',
|
|
63
|
+
variant = 'secondary',
|
|
64
|
+
onBlur,
|
|
65
|
+
onKeyDown,
|
|
66
|
+
onKeyUp,
|
|
67
|
+
onMouseDown,
|
|
68
|
+
onMouseLeave,
|
|
69
|
+
onMouseUp,
|
|
70
|
+
onTouchEnd,
|
|
71
|
+
onTouchStart,
|
|
72
|
+
...otherProps
|
|
73
|
+
} = props;
|
|
74
|
+
|
|
75
|
+
useAuthUiRuntimeStyles();
|
|
76
|
+
|
|
77
|
+
const { animationClassName, eventHandlers } = useButtonAnimation(
|
|
78
|
+
disabled || loading ? null : animation
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const mergedClassName = mergeClassNames(
|
|
82
|
+
'sublime-button',
|
|
83
|
+
`sublime-button--${variant}`,
|
|
84
|
+
animationClassName,
|
|
85
|
+
className
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const visualContent = loading
|
|
89
|
+
? <ButtonLoadingIndicator animation={loadingAnimation} size={18} />
|
|
90
|
+
: leadingVisual ?? (isIconOnly ? children : undefined);
|
|
91
|
+
|
|
92
|
+
const labelContent = isIconOnly ? null : children;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Button
|
|
96
|
+
ref={forwardedRef}
|
|
97
|
+
type={type}
|
|
98
|
+
disabled={disabled || loading}
|
|
99
|
+
className={mergedClassName}
|
|
100
|
+
data-icon-only={isIconOnly ? 'true' : undefined}
|
|
101
|
+
data-loading={loading ? 'true' : undefined}
|
|
102
|
+
aria-busy={loading ? true : ariaBusy}
|
|
103
|
+
{...otherProps}
|
|
104
|
+
onBlur={composeEventHandlers(eventHandlers.onBlur, onBlur)}
|
|
105
|
+
onKeyDown={composeEventHandlers(eventHandlers.onKeyDown, onKeyDown)}
|
|
106
|
+
onKeyUp={composeEventHandlers(eventHandlers.onKeyUp, onKeyUp)}
|
|
107
|
+
onMouseDown={composeEventHandlers(eventHandlers.onMouseDown, onMouseDown)}
|
|
108
|
+
onMouseLeave={composeEventHandlers(eventHandlers.onMouseLeave, onMouseLeave)}
|
|
109
|
+
onMouseUp={composeEventHandlers(eventHandlers.onMouseUp, onMouseUp)}
|
|
110
|
+
onTouchEnd={composeEventHandlers(eventHandlers.onTouchEnd, onTouchEnd)}
|
|
111
|
+
onTouchStart={composeEventHandlers(eventHandlers.onTouchStart, onTouchStart)}
|
|
112
|
+
>
|
|
113
|
+
<span className="sublime-button__content">
|
|
114
|
+
{visualContent ? (
|
|
115
|
+
<span className="sublime-button__visual">{visualContent}</span>
|
|
116
|
+
) : null}
|
|
117
|
+
{labelContent !== null && labelContent !== undefined ? (
|
|
118
|
+
<span className="sublime-button__label">{labelContent}</span>
|
|
119
|
+
) : null}
|
|
120
|
+
</span>
|
|
121
|
+
</Button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SpinnerIcon } from './icons';
|
|
4
|
+
import type { ButtonLoadingAnimation } from './types';
|
|
5
|
+
|
|
6
|
+
interface ButtonLoadingIndicatorProps {
|
|
7
|
+
animation?: ButtonLoadingAnimation;
|
|
8
|
+
size?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface LoadingIndicatorVisualProps {
|
|
12
|
+
size: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SpinnerLoadingIndicator({ size }: LoadingIndicatorVisualProps) {
|
|
16
|
+
return <SpinnerIcon size={size} className="sublime-button__spinner" />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LOADING_INDICATORS: Record<
|
|
20
|
+
ButtonLoadingAnimation,
|
|
21
|
+
(props: LoadingIndicatorVisualProps) => ReactNode
|
|
22
|
+
> = {
|
|
23
|
+
spinner: SpinnerLoadingIndicator,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function ButtonLoadingIndicator({
|
|
27
|
+
animation = 'spinner',
|
|
28
|
+
size = 18,
|
|
29
|
+
}: ButtonLoadingIndicatorProps) {
|
|
30
|
+
const LoadingIndicator = LOADING_INDICATORS[animation];
|
|
31
|
+
|
|
32
|
+
return <LoadingIndicator size={size} />;
|
|
33
|
+
}
|
package/src/icons.tsx
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
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
|
/**
|
|
@@ -16,6 +13,7 @@ function getSafeSize(size: number | undefined): number {
|
|
|
16
13
|
*/
|
|
17
14
|
export function GoogleIcon({ size, className }: OAuthIconsProps) {
|
|
18
15
|
const safeSize = getSafeSize(size);
|
|
16
|
+
|
|
19
17
|
return (
|
|
20
18
|
<svg
|
|
21
19
|
width={safeSize}
|
|
@@ -23,8 +21,8 @@ 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"
|
|
25
|
+
className={className}
|
|
28
26
|
>
|
|
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"
|
|
@@ -52,6 +50,7 @@ export function GoogleIcon({ size, className }: OAuthIconsProps) {
|
|
|
52
50
|
*/
|
|
53
51
|
export function DiscordIcon({ size, className }: OAuthIconsProps) {
|
|
54
52
|
const safeSize = getSafeSize(size);
|
|
53
|
+
|
|
55
54
|
return (
|
|
56
55
|
<svg
|
|
57
56
|
width={safeSize}
|
|
@@ -59,8 +58,8 @@ export function DiscordIcon({ size, className }: OAuthIconsProps) {
|
|
|
59
58
|
viewBox="0 0 24 24"
|
|
60
59
|
fill="none"
|
|
61
60
|
xmlns="http://www.w3.org/2000/svg"
|
|
62
|
-
className={className}
|
|
63
61
|
aria-hidden="true"
|
|
62
|
+
className={className}
|
|
64
63
|
>
|
|
65
64
|
<path
|
|
66
65
|
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"
|
|
@@ -71,48 +70,36 @@ export function DiscordIcon({ size, className }: OAuthIconsProps) {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
/**
|
|
74
|
-
* Spinner Icon for loading state
|
|
73
|
+
* Spinner Icon for loading state.
|
|
74
|
+
* Motion is applied through package runtime CSS so the icon stays responsive.
|
|
75
75
|
*/
|
|
76
76
|
export function SpinnerIcon({ size, className }: OAuthIconsProps) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const finalSize = Math.max(1, safeSize);
|
|
77
|
+
const safeSize = getSafeSize(size, 20);
|
|
78
|
+
|
|
80
79
|
return (
|
|
81
80
|
<svg
|
|
82
|
-
width={
|
|
83
|
-
height={
|
|
81
|
+
width={safeSize}
|
|
82
|
+
height={safeSize}
|
|
84
83
|
viewBox="0 0 24 24"
|
|
85
84
|
fill="none"
|
|
86
85
|
xmlns="http://www.w3.org/2000/svg"
|
|
87
|
-
className={className}
|
|
88
86
|
aria-hidden="true"
|
|
87
|
+
className={className}
|
|
89
88
|
>
|
|
90
89
|
<circle
|
|
91
90
|
cx="12"
|
|
92
91
|
cy="12"
|
|
93
|
-
r="
|
|
92
|
+
r="9"
|
|
94
93
|
stroke="currentColor"
|
|
95
|
-
strokeWidth="
|
|
96
|
-
|
|
97
|
-
strokeDasharray="60"
|
|
98
|
-
strokeDashoffset="20"
|
|
99
|
-
opacity="0.25"
|
|
94
|
+
strokeWidth="3"
|
|
95
|
+
opacity="0.22"
|
|
100
96
|
/>
|
|
101
97
|
<path
|
|
102
|
-
d="M12
|
|
98
|
+
d="M12 3a9 9 0 0 1 9 9"
|
|
103
99
|
stroke="currentColor"
|
|
104
|
-
strokeWidth="
|
|
100
|
+
strokeWidth="3"
|
|
105
101
|
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>
|
|
102
|
+
/>
|
|
116
103
|
</svg>
|
|
117
104
|
);
|
|
118
|
-
}
|
|
105
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @sublimee/auth-ui
|
|
3
|
-
*
|
|
3
|
+
* Ready-to-use authentication UI components for Sublime.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Philosophy:
|
|
6
|
+
* -----------
|
|
7
|
+
* Auth components in this package are meant to be visible, polished,
|
|
8
|
+
* and useful on first render. They are semantic and adaptable, not
|
|
9
|
+
* intentionally bare or headless.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Quick Start:
|
|
12
|
+
* -----------
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import '@sublimee/tokens/tokens.css';
|
|
15
|
+
* import { OAuthButton } from '@sublimee/auth-ui';
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* <OAuthButton provider="google" />
|
|
18
|
+
* <OAuthButton provider="discord" variant="primary" loading />
|
|
19
|
+
* ```
|
|
17
20
|
*
|
|
18
|
-
*
|
|
21
|
+
* The package injects its runtime button styles automatically so the
|
|
22
|
+
* default experience stays zero-setup for consumers.
|
|
23
|
+
*
|
|
24
|
+
* @packageDocumentation
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
|
-
// Components
|
|
22
27
|
export { OAuthButton } from './oauth-button';
|
|
23
|
-
export {
|
|
24
|
-
|
|
25
|
-
// Icons
|
|
26
|
-
export { GoogleIcon, DiscordIcon, SpinnerIcon } from './icons';
|
|
27
|
-
|
|
28
|
-
// Types
|
|
28
|
+
export { useButtonAnimation } from './use-button-animation';
|
|
29
|
+
export { GoogleIcon, DiscordIcon } from './icons';
|
|
29
30
|
export type {
|
|
30
31
|
OAuthProvider,
|
|
31
32
|
OAuthButtonProps,
|
|
32
33
|
OAuthIconsProps,
|
|
33
|
-
|
|
34
|
+
ButtonVariant,
|
|
35
|
+
ButtonAnimation,
|
|
36
|
+
ButtonLoadingAnimation,
|
|
37
|
+
UseButtonAnimationResult,
|
|
38
|
+
} from './types';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Story } from '@ladle/react';
|
|
2
|
+
|
|
3
|
+
import { OAuthButton } from './oauth-button';
|
|
4
|
+
import type { OAuthProvider, ButtonVariant } from './types';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'B Auth Button',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* OAuthButton stories
|
|
12
|
+
*
|
|
13
|
+
* Semantic OAuth buttons with curated motion and built-in loading behavior.
|
|
14
|
+
* These components adapt to your theme via CSS custom properties.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface OAuthButtonStoryProps {
|
|
18
|
+
provider: OAuthProvider;
|
|
19
|
+
variant: ButtonVariant;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Default: Story<OAuthButtonStoryProps> = ({
|
|
26
|
+
provider,
|
|
27
|
+
variant,
|
|
28
|
+
disabled,
|
|
29
|
+
loading,
|
|
30
|
+
label,
|
|
31
|
+
}) => (
|
|
32
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
33
|
+
<OAuthButton
|
|
34
|
+
provider={provider}
|
|
35
|
+
variant={variant}
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
loading={loading}
|
|
38
|
+
>
|
|
39
|
+
{label || undefined}
|
|
40
|
+
</OAuthButton>
|
|
41
|
+
|
|
42
|
+
<OAuthButton
|
|
43
|
+
provider={provider}
|
|
44
|
+
variant="secondary"
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
loading={loading}
|
|
47
|
+
>
|
|
48
|
+
{null}
|
|
49
|
+
</OAuthButton>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
Default.args = {
|
|
54
|
+
provider: 'google',
|
|
55
|
+
variant: 'secondary',
|
|
56
|
+
disabled: false,
|
|
57
|
+
loading: false,
|
|
58
|
+
label: '',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
Default.argTypes = {
|
|
62
|
+
provider: {
|
|
63
|
+
control: { type: 'select' },
|
|
64
|
+
options: ['google', 'discord'],
|
|
65
|
+
},
|
|
66
|
+
variant: {
|
|
67
|
+
control: { type: 'select' },
|
|
68
|
+
options: ['primary', 'secondary'],
|
|
69
|
+
},
|
|
70
|
+
label: {
|
|
71
|
+
control: { type: 'text' },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const AllVariants: Story = () => (
|
|
76
|
+
<div className="flex flex-col gap-4 items-start">
|
|
77
|
+
<div className="space-y-2">
|
|
78
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Primary</p>
|
|
79
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
80
|
+
<OAuthButton provider="google" variant="primary" />
|
|
81
|
+
<OAuthButton provider="discord" variant="primary" />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Secondary (Default)</p>
|
|
87
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
88
|
+
<OAuthButton provider="google" variant="secondary" />
|
|
89
|
+
<OAuthButton provider="discord" variant="secondary" />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const States: Story = () => (
|
|
96
|
+
<div className="flex flex-col gap-4 items-start">
|
|
97
|
+
<div className="space-y-2">
|
|
98
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Default</p>
|
|
99
|
+
<OAuthButton provider="google" />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="space-y-2">
|
|
103
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Disabled</p>
|
|
104
|
+
<OAuthButton provider="google" disabled />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="space-y-2">
|
|
108
|
+
<p className="text-sm font-medium text-[var(--sublime-color-text-muted)]">Loading</p>
|
|
109
|
+
<OAuthButton provider="google" loading />
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export const DarkMode: Story = () => (
|
|
115
|
+
<div className="dark bg-[var(--sublime-color-surface-0)] p-8 rounded-lg">
|
|
116
|
+
<div className="flex flex-col gap-4 items-start">
|
|
117
|
+
<p className="text-[var(--sublime-color-text-primary)] mb-4">Dark Mode Preview</p>
|
|
118
|
+
|
|
119
|
+
<OAuthButton provider="google" variant="primary" />
|
|
120
|
+
<OAuthButton provider="google" variant="secondary" />
|
|
121
|
+
<OAuthButton provider="discord" variant="secondary" loading />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|