@oxyhq/services 13.1.3 → 13.1.5
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/lib/commonjs/index.js +1 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileButton.js +129 -14
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +18 -1
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +129 -14
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +18 -1
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/index.js +1 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +6 -8
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +6 -8
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/types/react-native-web-style.d.ts +27 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/types/react-native-web-style.d.ts +27 -0
- package/src/ui/components/ProfileButton.tsx +138 -14
- package/src/ui/components/ProfileMenu.tsx +16 -1
- package/src/ui/index.ts +1 -0
|
@@ -28,6 +28,33 @@ const MENU_WIDTH = 300;
|
|
|
28
28
|
/** Gutter, in px, kept between the popover and the viewport edges on web. */
|
|
29
29
|
const VIEWPORT_GUTTER = 8;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Web-only Bluesky-style hover animation timings, mirrored from
|
|
33
|
+
* `social-app/src/view/shell/desktop/LeftNav.tsx`. The row background and the
|
|
34
|
+
* name/handle/chevron opacity cross-fade quickly; the avatar shrink+slide is
|
|
35
|
+
* slower and more deliberate. A short delay on the LEAVE transition keeps the
|
|
36
|
+
* row from flickering when the pointer crosses a child boundary.
|
|
37
|
+
*/
|
|
38
|
+
const BG_TRANSITION_MS = 150;
|
|
39
|
+
const AVATAR_TRANSITION_MS = 250;
|
|
40
|
+
const OPACITY_TRANSITION_MS = 150;
|
|
41
|
+
const LEAVE_DELAY_MS = 50;
|
|
42
|
+
|
|
43
|
+
/** Shrunk-avatar scale on hover, matching Bluesky's `scale: 2/3`. */
|
|
44
|
+
const ACTIVE_AVATAR_SCALE = 2 / 3;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads the OS "reduce motion" preference on web. Guards `window`/`matchMedia`
|
|
48
|
+
* so it is safe during SSR and on native (where it is never called). When
|
|
49
|
+
* reduced motion is on, the avatar transform snaps instead of transitioning.
|
|
50
|
+
*/
|
|
51
|
+
const prefersReducedMotion = (): boolean => {
|
|
52
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
56
|
+
};
|
|
57
|
+
|
|
31
58
|
export interface ProfileButtonProps {
|
|
32
59
|
/**
|
|
33
60
|
* Expanded row (avatar + name + handle + chevron) when `true` (default), or a
|
|
@@ -93,6 +120,11 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
93
120
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
94
121
|
const [anchor, setAnchor] = useState<ProfileMenuAnchor | null>(null);
|
|
95
122
|
|
|
123
|
+
// Web-only hover/focus tracking for the Bluesky-style reveal animation.
|
|
124
|
+
// Native has no hover, so these stay false and the row renders statically.
|
|
125
|
+
const [hovered, setHovered] = useState(false);
|
|
126
|
+
const [focused, setFocused] = useState(false);
|
|
127
|
+
|
|
96
128
|
// Trigger ref for the web anchor measurement. RN-Web exposes `measure()` on
|
|
97
129
|
// host views, so we anchor the popover to the button rect and open upward.
|
|
98
130
|
const triggerRef = useRef<View | null>(null);
|
|
@@ -159,7 +191,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
159
191
|
const signInLabel = t('common.actions.signIn') || 'Sign in';
|
|
160
192
|
return (
|
|
161
193
|
<Pressable
|
|
162
|
-
className={
|
|
194
|
+
className={`${expanded ? 'w-full ' : ''}flex-row items-center gap-3 rounded-full px-2 py-2 ${className ?? ''}`}
|
|
163
195
|
style={style}
|
|
164
196
|
onPress={() => { void signIn(); }}
|
|
165
197
|
accessibilityRole="button"
|
|
@@ -201,16 +233,46 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
201
233
|
/>
|
|
202
234
|
);
|
|
203
235
|
|
|
236
|
+
// `active` combines hover, keyboard focus, and menu-open — exactly like
|
|
237
|
+
// Bluesky's `state.hovered || state.focused || control.isOpen`. Only web
|
|
238
|
+
// ever sets `hovered`/`focused`, so on native this reduces to `menuOpen`.
|
|
239
|
+
const active = hovered || focused || menuOpen;
|
|
240
|
+
const accountLabel = t('accountSwitcher.switchWhileSignedInAs', { name: displayName })
|
|
241
|
+
|| `Switch account, signed in as ${displayName}`;
|
|
242
|
+
|
|
243
|
+
// Web-only pointer/focus handlers. RN-Web forwards `onHoverIn`/`onHoverOut`
|
|
244
|
+
// to the underlying element; native Pressable ignores them harmlessly, but
|
|
245
|
+
// we only attach them on web to keep the native path pristine.
|
|
246
|
+
const webInteractionProps = isWeb
|
|
247
|
+
? {
|
|
248
|
+
onHoverIn: () => setHovered(true),
|
|
249
|
+
onHoverOut: () => setHovered(false),
|
|
250
|
+
onFocus: () => setFocused(true),
|
|
251
|
+
onBlur: () => setFocused(false),
|
|
252
|
+
}
|
|
253
|
+
: undefined;
|
|
254
|
+
|
|
204
255
|
if (!expanded) {
|
|
256
|
+
// Collapsed: avatar-only. On web, hovering just fades a subtle round
|
|
257
|
+
// background behind the avatar — no shrink, no text.
|
|
258
|
+
const collapsedBgStyle: ViewStyle | undefined = isWeb
|
|
259
|
+
? {
|
|
260
|
+
backgroundColor: active ? colors.backgroundSecondary : 'transparent',
|
|
261
|
+
transitionProperty: 'background-color',
|
|
262
|
+
transitionDuration: `${BG_TRANSITION_MS}ms`,
|
|
263
|
+
transitionDelay: active ? '0ms' : `${LEAVE_DELAY_MS}ms`,
|
|
264
|
+
}
|
|
265
|
+
: undefined;
|
|
205
266
|
return (
|
|
206
267
|
<View className={className} style={style}>
|
|
207
268
|
<Pressable
|
|
208
269
|
ref={triggerRef}
|
|
209
270
|
className="rounded-full"
|
|
271
|
+
style={collapsedBgStyle}
|
|
210
272
|
onPress={openMenu}
|
|
211
273
|
accessibilityRole="button"
|
|
212
|
-
accessibilityLabel={
|
|
213
|
-
|
|
274
|
+
accessibilityLabel={accountLabel}
|
|
275
|
+
{...webInteractionProps}
|
|
214
276
|
>
|
|
215
277
|
{avatarNode}
|
|
216
278
|
</Pressable>
|
|
@@ -227,18 +289,78 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
227
289
|
);
|
|
228
290
|
}
|
|
229
291
|
|
|
292
|
+
// ── Expanded row animation ──────────────────────────────────────────────
|
|
293
|
+
// On native everything is statically visible (no hover exists). On web we
|
|
294
|
+
// reproduce Bluesky's reveal: the row fills with a subtle contrast bg, the
|
|
295
|
+
// avatar shrinks + slides left, and the name/handle/chevron fade in. The
|
|
296
|
+
// negative left margin tucks the identity block under the shrunk avatar so
|
|
297
|
+
// it slides out from behind it as the avatar contracts.
|
|
298
|
+
const reducedMotion = isWeb && prefersReducedMotion();
|
|
299
|
+
|
|
300
|
+
// Slide the shrunk avatar left so its visual left edge stays put as it
|
|
301
|
+
// contracts. The avatar loses `size * (1 - scale)` of width; half of that
|
|
302
|
+
// (the left half, since scale is centered) is the offset, plus the row's
|
|
303
|
+
// left padding (px-2 = 8px) so it hugs the row edge like Bluesky's.
|
|
304
|
+
const activeAvatarTranslateX =
|
|
305
|
+
-(resolvedAvatarSize * (1 - ACTIVE_AVATAR_SCALE)) / 2 - 8;
|
|
306
|
+
|
|
307
|
+
const rowStyle: StyleProp<ViewStyle> = isWeb
|
|
308
|
+
? {
|
|
309
|
+
backgroundColor: active ? colors.backgroundSecondary : 'transparent',
|
|
310
|
+
transitionProperty: 'background-color',
|
|
311
|
+
transitionDuration: `${BG_TRANSITION_MS}ms`,
|
|
312
|
+
transitionDelay: active ? '0ms' : `${LEAVE_DELAY_MS}ms`,
|
|
313
|
+
}
|
|
314
|
+
: undefined;
|
|
315
|
+
|
|
316
|
+
const avatarWrapperStyle: ViewStyle | undefined = isWeb
|
|
317
|
+
? {
|
|
318
|
+
zIndex: 10,
|
|
319
|
+
...(reducedMotion
|
|
320
|
+
? {}
|
|
321
|
+
: {
|
|
322
|
+
transitionProperty: 'transform',
|
|
323
|
+
transitionDuration: `${AVATAR_TRANSITION_MS}ms`,
|
|
324
|
+
transitionDelay: active ? '0ms' : `${LEAVE_DELAY_MS}ms`,
|
|
325
|
+
}),
|
|
326
|
+
transform: active
|
|
327
|
+
? [{ scale: ACTIVE_AVATAR_SCALE }, { translateX: activeAvatarTranslateX }]
|
|
328
|
+
: [{ scale: 1 }, { translateX: 0 }],
|
|
329
|
+
}
|
|
330
|
+
: undefined;
|
|
331
|
+
|
|
332
|
+
const identityStyle: ViewStyle | undefined = isWeb
|
|
333
|
+
? {
|
|
334
|
+
marginLeft: -resolvedAvatarSize / 2,
|
|
335
|
+
opacity: active ? 1 : 0,
|
|
336
|
+
transitionProperty: 'opacity',
|
|
337
|
+
transitionDuration: `${OPACITY_TRANSITION_MS}ms`,
|
|
338
|
+
transitionDelay: active ? '0ms' : `${LEAVE_DELAY_MS}ms`,
|
|
339
|
+
}
|
|
340
|
+
: undefined;
|
|
341
|
+
|
|
342
|
+
const chevronStyle: ViewStyle | undefined = isWeb
|
|
343
|
+
? {
|
|
344
|
+
opacity: active ? 1 : 0,
|
|
345
|
+
transitionProperty: 'opacity',
|
|
346
|
+
transitionDuration: `${OPACITY_TRANSITION_MS}ms`,
|
|
347
|
+
transitionDelay: active ? '0ms' : `${LEAVE_DELAY_MS}ms`,
|
|
348
|
+
}
|
|
349
|
+
: undefined;
|
|
350
|
+
|
|
230
351
|
return (
|
|
231
|
-
<View className={className} style={style}>
|
|
352
|
+
<View className={`w-full ${className ?? ''}`} style={style}>
|
|
232
353
|
<Pressable
|
|
233
354
|
ref={triggerRef}
|
|
234
|
-
className="flex-row items-center gap-3 rounded-full px-2 py-2"
|
|
355
|
+
className="w-full flex-row items-center gap-3 rounded-full px-2 py-2"
|
|
356
|
+
style={rowStyle}
|
|
235
357
|
onPress={openMenu}
|
|
236
358
|
accessibilityRole="button"
|
|
237
|
-
accessibilityLabel={
|
|
238
|
-
|
|
359
|
+
accessibilityLabel={accountLabel}
|
|
360
|
+
{...webInteractionProps}
|
|
239
361
|
>
|
|
240
|
-
{avatarNode}
|
|
241
|
-
<View className="min-w-0 flex-1">
|
|
362
|
+
<View style={avatarWrapperStyle}>{avatarNode}</View>
|
|
363
|
+
<View className="min-w-0 flex-1" style={identityStyle}>
|
|
242
364
|
<Text className="font-bold text-foreground" numberOfLines={1}>
|
|
243
365
|
{displayName}
|
|
244
366
|
</Text>
|
|
@@ -248,11 +370,13 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
248
370
|
</Text>
|
|
249
371
|
) : null}
|
|
250
372
|
</View>
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
373
|
+
<View style={chevronStyle}>
|
|
374
|
+
<MaterialCommunityIcons
|
|
375
|
+
name="unfold-more-horizontal"
|
|
376
|
+
size={18}
|
|
377
|
+
color={colors.textSecondary}
|
|
378
|
+
/>
|
|
379
|
+
</View>
|
|
256
380
|
</Pressable>
|
|
257
381
|
<ProfileMenu
|
|
258
382
|
open={menuOpen}
|
|
@@ -62,7 +62,7 @@ export interface ProfileMenuProps {
|
|
|
62
62
|
* Renders as an upward-opening popover anchored to the trigger on web, and a
|
|
63
63
|
* bottom-sheet style modal on native.
|
|
64
64
|
*/
|
|
65
|
-
const
|
|
65
|
+
const ProfileMenuBody: React.FC<ProfileMenuProps> = ({
|
|
66
66
|
open,
|
|
67
67
|
onClose,
|
|
68
68
|
anchor,
|
|
@@ -413,4 +413,19 @@ const styles = {
|
|
|
413
413
|
} satisfies ViewStyle,
|
|
414
414
|
};
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Public wrapper: renders nothing — and runs NO hooks — until `open`. Mounting
|
|
418
|
+
* the body only while open keeps the account hooks (`useDeviceAccounts` /
|
|
419
|
+
* refresh-all, Bloom dialog/toast controls) off the render path when the menu is
|
|
420
|
+
* closed, which is every time the sidebar or native drawer mounts
|
|
421
|
+
* `ProfileButton`. A hook that is unsafe on a given platform (e.g. throws during
|
|
422
|
+
* a native render) therefore never runs unless the user actually opens the menu.
|
|
423
|
+
*/
|
|
424
|
+
const ProfileMenu: React.FC<ProfileMenuProps> = (props) => {
|
|
425
|
+
if (!props.open) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return <ProfileMenuBody {...props} />;
|
|
429
|
+
};
|
|
430
|
+
|
|
416
431
|
export default ProfileMenu;
|