@sublimee/auth-ui 0.1.1 → 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.
@@ -0,0 +1,225 @@
1
+ 'use client';
2
+
3
+ import { useInsertionEffect } from 'react';
4
+
5
+ const AUTH_UI_RUNTIME_STYLE_ID = 'sublime-auth-ui-runtime-styles';
6
+
7
+ const AUTH_UI_RUNTIME_STYLES = `
8
+ .sublime-button {
9
+ --sublime-button-gap: var(--sublime-space-3, 0.75rem);
10
+ --sublime-button-padding-x: var(--sublime-space-button-x, 1rem);
11
+ --sublime-button-padding-y: var(--sublime-space-button-y, 0.75rem);
12
+ --sublime-button-height: var(--sublime-size-button-height-md, 2.75rem);
13
+ --sublime-button-radius: var(--sublime-radius-button, 0.875rem);
14
+ --sublime-button-font-family: var(--sublime-font-family-sans, ui-sans-serif, system-ui, sans-serif);
15
+ --sublime-button-font-size: var(--sublime-font-size-sm, 0.875rem);
16
+ --sublime-button-font-weight: var(--sublime-font-weight-medium, 500);
17
+ --sublime-button-line-height: var(--sublime-line-height-tight, 1.15);
18
+ --sublime-button-visual-size: 1.25rem;
19
+ --sublime-button-transition: var(--sublime-transition-button, background-color 160ms cubic-bezier(0.4, 0, 0.2, 1), border-color 160ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 160ms cubic-bezier(0.4, 0, 0.2, 1), color 160ms cubic-bezier(0.4, 0, 0.2, 1), transform 160ms cubic-bezier(0.4, 0, 0.2, 1));
20
+ --sublime-button-bg: var(--sublime-color-surface-1, #ffffff);
21
+ --sublime-button-bg-hover: var(--sublime-color-surface-1-hover, #f8fafc);
22
+ --sublime-button-bg-active: var(--sublime-color-surface-1-active, #eef2f7);
23
+ --sublime-button-color: var(--sublime-color-text-primary, #111827);
24
+ --sublime-button-color-hover: var(--sublime-color-text-primary, #111827);
25
+ --sublime-button-border: var(--sublime-color-border-primary, #d1d5db);
26
+ --sublime-button-border-hover: var(--sublime-color-border-primary-hover, #9ca3af);
27
+ --sublime-button-border-active: var(--sublime-color-border-primary-active, #6b7280);
28
+ --sublime-button-shadow: var(--sublime-shadow-button, 0 1px 2px rgb(15 23 42 / 0.08));
29
+ --sublime-button-shadow-hover: var(--sublime-shadow-button-hover, 0 8px 20px rgb(15 23 42 / 0.12));
30
+ --sublime-button-shadow-active: var(--sublime-shadow-button-active, inset 0 1px 2px rgb(15 23 42 / 0.14));
31
+ --sublime-button-focus-ring: var(--sublime-color-focus-ring, var(--sublime-color-interactive-accent, #2563eb));
32
+ --sublime-button-focus-ring-offset: var(--sublime-color-focus-ring-offset, var(--sublime-color-surface-0, #ffffff));
33
+ position: relative;
34
+ display: inline-flex;
35
+ min-height: var(--sublime-button-height);
36
+ align-items: center;
37
+ justify-content: center;
38
+ gap: var(--sublime-button-gap);
39
+ padding: var(--sublime-button-padding-y) var(--sublime-button-padding-x);
40
+ border: 1px solid var(--sublime-button-border);
41
+ border-radius: var(--sublime-button-radius);
42
+ background: var(--sublime-button-bg);
43
+ color: var(--sublime-button-color);
44
+ box-shadow: var(--sublime-button-shadow);
45
+ font-family: var(--sublime-button-font-family);
46
+ font-size: var(--sublime-button-font-size);
47
+ font-weight: var(--sublime-button-font-weight);
48
+ line-height: var(--sublime-button-line-height);
49
+ cursor: pointer;
50
+ user-select: none;
51
+ text-decoration: none;
52
+ white-space: nowrap;
53
+ vertical-align: middle;
54
+ isolation: isolate;
55
+ overflow: hidden;
56
+ appearance: none;
57
+ -webkit-tap-highlight-color: transparent;
58
+ transition: var(--sublime-button-transition);
59
+ }
60
+
61
+ .sublime-button:hover:not(:disabled) {
62
+ background: var(--sublime-button-bg-hover);
63
+ color: var(--sublime-button-color-hover);
64
+ border-color: var(--sublime-button-border-hover);
65
+ box-shadow: var(--sublime-button-shadow-hover);
66
+ }
67
+
68
+ .sublime-button:active:not(:disabled) {
69
+ background: var(--sublime-button-bg-active);
70
+ border-color: var(--sublime-button-border-active);
71
+ box-shadow: var(--sublime-button-shadow-active);
72
+ }
73
+
74
+ .sublime-button:focus {
75
+ outline: none;
76
+ }
77
+
78
+ .sublime-button:focus-visible {
79
+ box-shadow:
80
+ 0 0 0 2px var(--sublime-button-focus-ring-offset),
81
+ 0 0 0 4px var(--sublime-button-focus-ring),
82
+ var(--sublime-button-shadow-hover);
83
+ }
84
+
85
+ .sublime-button:disabled {
86
+ cursor: not-allowed;
87
+ opacity: 0.6;
88
+ box-shadow: none;
89
+ }
90
+
91
+ .sublime-button[data-loading='true'] {
92
+ cursor: progress;
93
+ }
94
+
95
+ .sublime-button[data-loading='true']:disabled {
96
+ opacity: 0.78;
97
+ }
98
+
99
+ .sublime-button[data-icon-only='true'] {
100
+ min-width: var(--sublime-button-height);
101
+ padding-inline: calc(var(--sublime-button-padding-y) + 0.125rem);
102
+ }
103
+
104
+ .sublime-button__content {
105
+ display: inline-flex;
106
+ width: 100%;
107
+ align-items: center;
108
+ justify-content: center;
109
+ gap: inherit;
110
+ }
111
+
112
+ .sublime-button__visual,
113
+ .sublime-button__label {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ }
117
+
118
+ .sublime-button__visual {
119
+ flex-shrink: 0;
120
+ justify-content: center;
121
+ min-width: var(--sublime-button-visual-size);
122
+ }
123
+
124
+ .sublime-button__visual svg {
125
+ display: block;
126
+ }
127
+
128
+ .sublime-button__label {
129
+ min-width: 0;
130
+ }
131
+
132
+ .sublime-button--primary {
133
+ --sublime-button-bg: var(--sublime-color-interactive-primary, #111827);
134
+ --sublime-button-bg-hover: var(--sublime-color-interactive-primary-hover, #1f2937);
135
+ --sublime-button-bg-active: var(--sublime-color-interactive-primary-active, #0f172a);
136
+ --sublime-button-color: var(--sublime-color-interactive-primary-text, #f9fafb);
137
+ --sublime-button-color-hover: var(--sublime-color-interactive-primary-text, #f9fafb);
138
+ --sublime-button-border: var(--sublime-color-interactive-primary, #111827);
139
+ --sublime-button-border-hover: var(--sublime-color-interactive-primary-hover, #1f2937);
140
+ --sublime-button-border-active: var(--sublime-color-interactive-primary-active, #0f172a);
141
+ }
142
+
143
+ .sublime-button--secondary {
144
+ --sublime-button-bg: var(--sublime-color-surface-1, #ffffff);
145
+ --sublime-button-bg-hover: var(--sublime-color-surface-1-hover, #f8fafc);
146
+ --sublime-button-bg-active: var(--sublime-color-surface-1-active, #eef2f7);
147
+ --sublime-button-color: var(--sublime-color-text-primary, #111827);
148
+ --sublime-button-color-hover: var(--sublime-color-text-primary, #111827);
149
+ --sublime-button-border: var(--sublime-color-border-primary, #d1d5db);
150
+ --sublime-button-border-hover: var(--sublime-color-border-primary-hover, #9ca3af);
151
+ --sublime-button-border-active: var(--sublime-color-border-primary-active, #6b7280);
152
+ }
153
+
154
+ .sublime-auth-button[data-provider='discord'] .sublime-button__visual {
155
+ color: #5865f2;
156
+ }
157
+
158
+ .sublime-auth-button[data-loading='true'] .sublime-button__visual {
159
+ color: currentColor;
160
+ }
161
+
162
+ @keyframes sublime-button-press-release {
163
+ 0% {
164
+ transform: translateY(1px) scale(0.985);
165
+ box-shadow: var(--sublime-button-shadow-active);
166
+ }
167
+ 100% {
168
+ transform: translateY(0) scale(1);
169
+ box-shadow: var(--sublime-button-shadow);
170
+ }
171
+ }
172
+
173
+ @keyframes sublime-button-spinner {
174
+ from {
175
+ transform: rotate(0deg);
176
+ }
177
+ to {
178
+ transform: rotate(360deg);
179
+ }
180
+ }
181
+
182
+ .sublime-button-pressed {
183
+ transform: translateY(1px) scale(0.985);
184
+ box-shadow: var(--sublime-button-shadow-active);
185
+ }
186
+
187
+ .animate-sublime-button-press-release {
188
+ animation: sublime-button-press-release 180ms var(--sublime-ease-out, cubic-bezier(0, 0, 0.2, 1));
189
+ }
190
+
191
+ .sublime-button__spinner {
192
+ animation: sublime-button-spinner 720ms linear infinite;
193
+ transform-origin: center;
194
+ }
195
+
196
+ @media (prefers-reduced-motion: reduce) {
197
+ .sublime-button,
198
+ .animate-sublime-button-press-release {
199
+ animation-duration: 0.01ms !important;
200
+ animation-iteration-count: 1 !important;
201
+ transition-duration: 0.01ms !important;
202
+ }
203
+
204
+ .sublime-button__spinner {
205
+ animation-duration: 1.6s !important;
206
+ }
207
+ }
208
+ `;
209
+
210
+ export function useAuthUiRuntimeStyles() {
211
+ useInsertionEffect(() => {
212
+ if (typeof document === 'undefined') {
213
+ return;
214
+ }
215
+
216
+ if (document.getElementById(AUTH_UI_RUNTIME_STYLE_ID)) {
217
+ return;
218
+ }
219
+
220
+ const styleElement = document.createElement('style');
221
+ styleElement.id = AUTH_UI_RUNTIME_STYLE_ID;
222
+ styleElement.textContent = AUTH_UI_RUNTIME_STYLES;
223
+ document.head.prepend(styleElement);
224
+ }, []);
225
+ }
package/src/types.ts CHANGED
@@ -1,40 +1,96 @@
1
- import type { ReactNode, ButtonHTMLAttributes } from 'react';
1
+ import type {
2
+ ButtonHTMLAttributes,
3
+ MouseEventHandler,
4
+ ReactNode,
5
+ } from 'react';
2
6
 
3
7
  export type OAuthProvider = 'google' | 'discord';
4
8
 
9
+ export type ButtonVariant = 'primary' | 'secondary';
10
+
11
+ export type ButtonAnimation = 'press';
12
+
13
+ export type ButtonLoadingAnimation = 'spinner';
14
+
5
15
  export interface OAuthButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
6
16
  /**
7
- * The OAuth provider to authenticate with
17
+ * The OAuth provider to authenticate with.
8
18
  */
9
19
  provider: OAuthProvider;
10
20
 
11
21
  /**
12
- * Click handler - typically calls supabase.auth.signInWithOAuth
22
+ * Click handler - typically calls supabase.auth.signInWithOAuth.
13
23
  */
14
- onClick?: () => void;
24
+ onClick?: MouseEventHandler<HTMLButtonElement>;
15
25
 
16
26
  /**
17
- * Loading state - shows spinner when true
27
+ * Loading state - shows the configured loading animation and disables interaction.
18
28
  */
19
29
  loading?: boolean;
20
30
 
21
31
  /**
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"
32
+ * Loading animation shown while `loading` is true.
33
+ * @default 'spinner'
34
+ */
35
+ loadingAnimation?: ButtonLoadingAnimation;
36
+
37
+ /**
38
+ * Visual variant of the button.
39
+ * - primary: High emphasis, filled background
40
+ * - secondary: Medium emphasis, bordered (default)
41
+ * @default 'secondary'
42
+ */
43
+ variant?: ButtonVariant;
44
+
45
+ /**
46
+ * Interaction motion applied on press.
47
+ * @default 'press'
48
+ */
49
+ animation?: ButtonAnimation;
50
+
51
+ /**
52
+ * Custom className appended after the built-in runtime classes.
24
53
  */
25
54
  className?: string;
26
55
 
27
56
  /**
28
- * Button children - defaults to provider name
29
- * Pass `null` for icon-only mode
57
+ * Button children - defaults to provider name in Spanish.
58
+ * Pass `null` for icon-only mode.
30
59
  */
31
60
  children?: ReactNode;
32
61
  }
33
62
 
34
63
  export interface OAuthIconsProps {
35
64
  /**
36
- * Icon size in pixels (min: 1)
65
+ * Icon size in pixels (min: 1).
37
66
  * @default 24
38
67
  */
39
68
  size?: number;
69
+
70
+ /**
71
+ * Optional className for icon-specific styling.
72
+ */
73
+ className?: string;
40
74
  }
75
+
76
+ export interface UseButtonAnimationResult {
77
+ /**
78
+ * CSS classes to apply for the current animation state.
79
+ */
80
+ animationClassName: string;
81
+
82
+ /**
83
+ * Event handlers to attach to the button.
84
+ */
85
+ eventHandlers: Pick<
86
+ ButtonHTMLAttributes<HTMLButtonElement>,
87
+ | 'onBlur'
88
+ | 'onKeyDown'
89
+ | 'onKeyUp'
90
+ | 'onMouseDown'
91
+ | 'onMouseLeave'
92
+ | 'onMouseUp'
93
+ | 'onTouchEnd'
94
+ | 'onTouchStart'
95
+ >;
96
+ }
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import type { KeyboardEvent } from 'react';
5
+
6
+ import type { ButtonAnimation, UseButtonAnimationResult } from './types';
7
+
8
+ type AnimationPhase = 'idle' | 'pressed' | 'releasing';
9
+
10
+ const PRESS_RELEASE_DURATION = 180;
11
+
12
+ /**
13
+ * Hook to provide the curated button press animation.
14
+ *
15
+ * Handles mouse, touch, and keyboard interaction so motion feedback stays
16
+ * consistent across the different ways users can activate a button.
17
+ */
18
+ export function useButtonAnimation(
19
+ animation: ButtonAnimation | null = 'press'
20
+ ): UseButtonAnimationResult {
21
+ const [phase, setPhase] = useState<AnimationPhase>('idle');
22
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
23
+
24
+ const clearExistingTimeout = useCallback(() => {
25
+ if (timeoutRef.current) {
26
+ clearTimeout(timeoutRef.current);
27
+ timeoutRef.current = null;
28
+ }
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ return () => {
33
+ clearExistingTimeout();
34
+ };
35
+ }, [clearExistingTimeout]);
36
+
37
+ const handlePressStart = useCallback(() => {
38
+ if (!animation) {
39
+ return;
40
+ }
41
+
42
+ clearExistingTimeout();
43
+ setPhase('pressed');
44
+ }, [animation, clearExistingTimeout]);
45
+
46
+ const handlePressEnd = useCallback(() => {
47
+ if (!animation || phase !== 'pressed') {
48
+ return;
49
+ }
50
+
51
+ setPhase('releasing');
52
+
53
+ timeoutRef.current = setTimeout(() => {
54
+ setPhase('idle');
55
+ }, PRESS_RELEASE_DURATION);
56
+ }, [animation, phase]);
57
+
58
+ const handleBlur = useCallback(() => {
59
+ clearExistingTimeout();
60
+ setPhase('idle');
61
+ }, [clearExistingTimeout]);
62
+
63
+ const handleKeyDown = useCallback(
64
+ (event: KeyboardEvent<HTMLButtonElement>) => {
65
+ if (event.repeat) {
66
+ return;
67
+ }
68
+
69
+ if (event.key === ' ' || event.key === 'Enter') {
70
+ handlePressStart();
71
+ }
72
+ },
73
+ [handlePressStart]
74
+ );
75
+
76
+ const handleKeyUp = useCallback(
77
+ (event: KeyboardEvent<HTMLButtonElement>) => {
78
+ if (event.key === ' ' || event.key === 'Enter') {
79
+ handlePressEnd();
80
+ }
81
+ },
82
+ [handlePressEnd]
83
+ );
84
+
85
+ const getAnimationClass = (): string => {
86
+ if (!animation) {
87
+ return '';
88
+ }
89
+
90
+ if (phase === 'pressed') {
91
+ return 'sublime-button-pressed';
92
+ }
93
+
94
+ if (phase === 'releasing') {
95
+ return 'animate-sublime-button-press-release';
96
+ }
97
+
98
+ return '';
99
+ };
100
+
101
+ return {
102
+ animationClassName: getAnimationClass(),
103
+ eventHandlers: {
104
+ onBlur: handleBlur,
105
+ onKeyDown: handleKeyDown,
106
+ onKeyUp: handleKeyUp,
107
+ onMouseDown: handlePressStart,
108
+ onMouseLeave: handlePressEnd,
109
+ onMouseUp: handlePressEnd,
110
+ onTouchEnd: handlePressEnd,
111
+ onTouchStart: handlePressStart,
112
+ },
113
+ };
114
+ }