@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.
- package/AI_INDEX.md +64 -0
- package/README.md +80 -120
- package/dist/animations.css +52 -0
- package/dist/index.d.mts +59 -42
- package/dist/index.d.ts +59 -42
- package/dist/index.js +443 -56
- package/dist/index.mjs +441 -54
- 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 +17 -24
- package/src/index.ts +25 -19
- package/src/oauth-button.stories.tsx +72 -333
- package/src/oauth-button.tsx +34 -79
- package/src/runtime-styles.ts +225 -0
- package/src/types.ts +66 -10
- package/src/use-button-animation.ts +114 -0
|
@@ -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 {
|
|
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?:
|
|
24
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
15
25
|
|
|
16
26
|
/**
|
|
17
|
-
* Loading state - shows
|
|
27
|
+
* Loading state - shows the configured loading animation and disables interaction.
|
|
18
28
|
*/
|
|
19
29
|
loading?: boolean;
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
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
|
+
}
|