@oxyhq/services 13.1.7 → 13.3.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/lib/commonjs/ui/components/ProfileButton.js +107 -31
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +162 -38
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +108 -32
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +163 -39
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +23 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +9 -4
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts +23 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +9 -4
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ui/components/ProfileButton.tsx +133 -26
- package/src/ui/components/ProfileMenu.tsx +164 -43
|
@@ -3,6 +3,7 @@ import { useCallback, useRef, useState } from 'react';
|
|
|
3
3
|
import {
|
|
4
4
|
View,
|
|
5
5
|
Pressable,
|
|
6
|
+
StyleSheet,
|
|
6
7
|
Platform,
|
|
7
8
|
type StyleProp,
|
|
8
9
|
type ViewStyle,
|
|
@@ -73,7 +74,23 @@ export interface ProfileButtonProps {
|
|
|
73
74
|
onNavigateProfile?: () => void;
|
|
74
75
|
/** Called before the active identity changes so apps can clear scoped state. */
|
|
75
76
|
onBeforeSessionChange?: () => void | Promise<void>;
|
|
76
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Web-only popover direction relative to the trigger:
|
|
79
|
+
* - `'up'` (default): the menu opens UPWARD, for a trigger placed at the
|
|
80
|
+
* BOTTOM of a sidebar (the footer pattern). Pixel-identical to prior
|
|
81
|
+
* behavior.
|
|
82
|
+
* - `'down'`: the menu opens DOWNWARD, for a trigger placed at the TOP of a
|
|
83
|
+
* sidebar (the accounts app pattern).
|
|
84
|
+
* - `'auto'`: opens downward when there is more room below the trigger than
|
|
85
|
+
* above, otherwise upward.
|
|
86
|
+
* Native (bottom-sheet) is unaffected — this only influences the web popover.
|
|
87
|
+
*/
|
|
88
|
+
placement?: 'up' | 'down' | 'auto';
|
|
89
|
+
/**
|
|
90
|
+
* Extra className applied to the outer trigger. Kept for NativeWind consumers
|
|
91
|
+
* that layer utility classes on top; the component's own layout is driven by
|
|
92
|
+
* `StyleSheet` so it renders correctly with or without NativeWind.
|
|
93
|
+
*/
|
|
77
94
|
className?: string;
|
|
78
95
|
/** Extra style applied to the outer trigger. */
|
|
79
96
|
style?: StyleProp<ViewStyle>;
|
|
@@ -92,6 +109,12 @@ export interface ProfileButtonProps {
|
|
|
92
109
|
* against the measured trigger, or as a bottom sheet (native).
|
|
93
110
|
* - **Signed out**: a "Sign in" row that calls `useAuth().signIn()`.
|
|
94
111
|
*
|
|
112
|
+
* Styling uses react-native `StyleSheet` + the Bloom theme (via `useTheme`) so
|
|
113
|
+
* the layout renders identically in EVERY consumer — including apps that do not
|
|
114
|
+
* use NativeWind (e.g. the accounts app). Only the web hover animation keeps
|
|
115
|
+
* dynamic inline `style` (the CSS transition/transform values), which is what
|
|
116
|
+
* the `react-native-web-style.d.ts` augmentation exists for.
|
|
117
|
+
*
|
|
95
118
|
* All display strings resolve through `@oxyhq/core`'s
|
|
96
119
|
* `getAccountDisplayName` / `getAccountFallbackHandle` — no hand-rolled name
|
|
97
120
|
* fallbacks. Avatars pass the bare file id as `source` + `variant="thumb"`, so
|
|
@@ -104,6 +127,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
104
127
|
onAddAccount,
|
|
105
128
|
onNavigateProfile,
|
|
106
129
|
onBeforeSessionChange,
|
|
130
|
+
placement = 'up',
|
|
107
131
|
className,
|
|
108
132
|
style,
|
|
109
133
|
}) => {
|
|
@@ -143,7 +167,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
143
167
|
setMenuOpen(true);
|
|
144
168
|
return;
|
|
145
169
|
}
|
|
146
|
-
node.measure((_x, _y, _w,
|
|
170
|
+
node.measure((_x, _y, _w, height, pageX, pageY) => {
|
|
147
171
|
if (typeof pageX !== 'number' || typeof pageY !== 'number') {
|
|
148
172
|
setAnchor(null);
|
|
149
173
|
} else {
|
|
@@ -153,17 +177,33 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
153
177
|
window.innerWidth - MENU_WIDTH - VIEWPORT_GUTTER,
|
|
154
178
|
);
|
|
155
179
|
const left = Math.min(Math.max(VIEWPORT_GUTTER, pageX), maxLeft);
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
// Bottom edge of the trigger in page coordinates. `height` is a
|
|
181
|
+
// number for host views; guard defensively for non-web shims.
|
|
182
|
+
const triggerBottom = pageY + (typeof height === 'number' ? height : 0);
|
|
183
|
+
// Resolve `'auto'` by comparing the room below the trigger with
|
|
184
|
+
// the room above it; pick whichever direction has more space.
|
|
185
|
+
const openDown =
|
|
186
|
+
placement === 'down' ||
|
|
187
|
+
(placement === 'auto' &&
|
|
188
|
+
window.innerHeight - triggerBottom > pageY);
|
|
189
|
+
if (openDown) {
|
|
190
|
+
// Anchor the panel's TOP edge just below the trigger bottom so
|
|
191
|
+
// the menu opens downward from a top-of-sidebar trigger.
|
|
192
|
+
const top = Math.max(VIEWPORT_GUTTER, triggerBottom + VIEWPORT_GUTTER);
|
|
193
|
+
setAnchor({ left, top });
|
|
194
|
+
} else {
|
|
195
|
+
// Anchor the panel's BOTTOM edge just above the trigger top so
|
|
196
|
+
// the menu opens upward from this footer button.
|
|
197
|
+
const bottom = Math.max(
|
|
198
|
+
VIEWPORT_GUTTER,
|
|
199
|
+
window.innerHeight - pageY + VIEWPORT_GUTTER,
|
|
200
|
+
);
|
|
201
|
+
setAnchor({ left, bottom });
|
|
202
|
+
}
|
|
163
203
|
}
|
|
164
204
|
setMenuOpen(true);
|
|
165
205
|
});
|
|
166
|
-
}, []);
|
|
206
|
+
}, [placement]);
|
|
167
207
|
|
|
168
208
|
const handleClose = useCallback(() => {
|
|
169
209
|
setMenuOpen(false);
|
|
@@ -179,8 +219,14 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
179
219
|
importantForAccessibility="no-hide-descendants"
|
|
180
220
|
>
|
|
181
221
|
<View
|
|
182
|
-
|
|
183
|
-
|
|
222
|
+
style={[
|
|
223
|
+
styles.skeletonCircle,
|
|
224
|
+
{
|
|
225
|
+
width: resolvedAvatarSize,
|
|
226
|
+
height: resolvedAvatarSize,
|
|
227
|
+
backgroundColor: colors.backgroundSecondary,
|
|
228
|
+
},
|
|
229
|
+
]}
|
|
184
230
|
/>
|
|
185
231
|
</View>
|
|
186
232
|
);
|
|
@@ -191,15 +237,21 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
191
237
|
const signInLabel = t('common.actions.signIn') || 'Sign in';
|
|
192
238
|
return (
|
|
193
239
|
<Pressable
|
|
194
|
-
className={
|
|
195
|
-
style={style}
|
|
240
|
+
className={className}
|
|
241
|
+
style={[styles.row, expanded && styles.rowExpanded, style]}
|
|
196
242
|
onPress={() => { void signIn(); }}
|
|
197
243
|
accessibilityRole="button"
|
|
198
244
|
accessibilityLabel={signInLabel}
|
|
199
245
|
>
|
|
200
246
|
<View
|
|
201
|
-
|
|
202
|
-
|
|
247
|
+
style={[
|
|
248
|
+
styles.avatarBadge,
|
|
249
|
+
{
|
|
250
|
+
width: resolvedAvatarSize,
|
|
251
|
+
height: resolvedAvatarSize,
|
|
252
|
+
backgroundColor: colors.backgroundSecondary,
|
|
253
|
+
},
|
|
254
|
+
]}
|
|
203
255
|
>
|
|
204
256
|
<MaterialCommunityIcons
|
|
205
257
|
name="login"
|
|
@@ -209,7 +261,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
209
261
|
</View>
|
|
210
262
|
{expanded ? (
|
|
211
263
|
<Text
|
|
212
|
-
|
|
264
|
+
style={[styles.signInLabel, { color: colors.text }]}
|
|
213
265
|
numberOfLines={1}
|
|
214
266
|
>
|
|
215
267
|
{signInLabel}
|
|
@@ -267,8 +319,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
267
319
|
<View className={className} style={style}>
|
|
268
320
|
<Pressable
|
|
269
321
|
ref={triggerRef}
|
|
270
|
-
|
|
271
|
-
style={collapsedBgStyle}
|
|
322
|
+
style={[styles.collapsedTrigger, collapsedBgStyle]}
|
|
272
323
|
onPress={openMenu}
|
|
273
324
|
accessibilityRole="button"
|
|
274
325
|
accessibilityLabel={accountLabel}
|
|
@@ -300,7 +351,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
300
351
|
// Slide the shrunk avatar left so its visual left edge stays put as it
|
|
301
352
|
// contracts. The avatar loses `size * (1 - scale)` of width; half of that
|
|
302
353
|
// (the left half, since scale is centered) is the offset, plus the row's
|
|
303
|
-
// left padding (
|
|
354
|
+
// left padding (8px) so it hugs the row edge like Bluesky's.
|
|
304
355
|
const activeAvatarTranslateX =
|
|
305
356
|
-(resolvedAvatarSize * (1 - ACTIVE_AVATAR_SCALE)) / 2 - 8;
|
|
306
357
|
|
|
@@ -349,23 +400,28 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
349
400
|
: undefined;
|
|
350
401
|
|
|
351
402
|
return (
|
|
352
|
-
<View className={
|
|
403
|
+
<View className={className} style={[styles.fullWidth, style]}>
|
|
353
404
|
<Pressable
|
|
354
405
|
ref={triggerRef}
|
|
355
|
-
|
|
356
|
-
style={rowStyle}
|
|
406
|
+
style={[styles.row, styles.rowExpanded, rowStyle]}
|
|
357
407
|
onPress={openMenu}
|
|
358
408
|
accessibilityRole="button"
|
|
359
409
|
accessibilityLabel={accountLabel}
|
|
360
410
|
{...webInteractionProps}
|
|
361
411
|
>
|
|
362
412
|
<View style={avatarWrapperStyle}>{avatarNode}</View>
|
|
363
|
-
<View
|
|
364
|
-
<Text
|
|
413
|
+
<View style={[styles.identity, identityStyle]}>
|
|
414
|
+
<Text
|
|
415
|
+
style={[styles.displayName, { color: colors.text }]}
|
|
416
|
+
numberOfLines={1}
|
|
417
|
+
>
|
|
365
418
|
{displayName}
|
|
366
419
|
</Text>
|
|
367
420
|
{handleLine ? (
|
|
368
|
-
<Text
|
|
421
|
+
<Text
|
|
422
|
+
style={[styles.handle, { color: colors.textSecondary }]}
|
|
423
|
+
numberOfLines={1}
|
|
424
|
+
>
|
|
369
425
|
{handleLine}
|
|
370
426
|
</Text>
|
|
371
427
|
) : null}
|
|
@@ -391,4 +447,55 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
391
447
|
);
|
|
392
448
|
};
|
|
393
449
|
|
|
450
|
+
const styles = StyleSheet.create({
|
|
451
|
+
fullWidth: {
|
|
452
|
+
width: '100%',
|
|
453
|
+
},
|
|
454
|
+
// Neutral skeleton / signed-out avatar circle (`rounded-full`).
|
|
455
|
+
skeletonCircle: {
|
|
456
|
+
borderRadius: 9999,
|
|
457
|
+
},
|
|
458
|
+
// Shared trigger row (`flex-row items-center gap-3 rounded-full px-2 py-2`).
|
|
459
|
+
row: {
|
|
460
|
+
flexDirection: 'row',
|
|
461
|
+
alignItems: 'center',
|
|
462
|
+
gap: 12,
|
|
463
|
+
borderRadius: 9999,
|
|
464
|
+
paddingHorizontal: 8,
|
|
465
|
+
paddingVertical: 8,
|
|
466
|
+
},
|
|
467
|
+
// `w-full` on the expanded row + its signed-out variant.
|
|
468
|
+
rowExpanded: {
|
|
469
|
+
width: '100%',
|
|
470
|
+
},
|
|
471
|
+
// Signed-out login badge (`items-center justify-center rounded-full`).
|
|
472
|
+
avatarBadge: {
|
|
473
|
+
alignItems: 'center',
|
|
474
|
+
justifyContent: 'center',
|
|
475
|
+
borderRadius: 9999,
|
|
476
|
+
},
|
|
477
|
+
// Signed-out label (`flex-1 font-semibold`).
|
|
478
|
+
signInLabel: {
|
|
479
|
+
flex: 1,
|
|
480
|
+
fontWeight: '600',
|
|
481
|
+
},
|
|
482
|
+
// Collapsed avatar-only trigger (`rounded-full`).
|
|
483
|
+
collapsedTrigger: {
|
|
484
|
+
borderRadius: 9999,
|
|
485
|
+
},
|
|
486
|
+
// Identity block (`min-w-0 flex-1`).
|
|
487
|
+
identity: {
|
|
488
|
+
flex: 1,
|
|
489
|
+
minWidth: 0,
|
|
490
|
+
},
|
|
491
|
+
// Display name (`font-bold`).
|
|
492
|
+
displayName: {
|
|
493
|
+
fontWeight: '700',
|
|
494
|
+
},
|
|
495
|
+
// Handle line (`text-xs`).
|
|
496
|
+
handle: {
|
|
497
|
+
fontSize: 12,
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
394
501
|
export default ProfileButton;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Modal,
|
|
7
7
|
ScrollView,
|
|
8
8
|
ActivityIndicator,
|
|
9
|
+
StyleSheet,
|
|
9
10
|
Platform,
|
|
10
11
|
type ViewStyle,
|
|
11
12
|
} from 'react-native';
|
|
@@ -26,15 +27,20 @@ const isWeb = Platform.OS === 'web';
|
|
|
26
27
|
const MENU_WIDTH = 300;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Web-only anchor. `ProfileButton` measures its trigger and anchors the panel
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Web-only anchor. `ProfileButton` measures its trigger and anchors the panel to
|
|
31
|
+
* one corner so the menu opens either UPWARD (footer trigger) or DOWNWARD (a
|
|
32
|
+
* trigger at the top of a sidebar). Exactly ONE vertical edge is pinned:
|
|
33
|
+
* - `bottom` set → the panel's BOTTOM edge is anchored, so it opens UPWARD.
|
|
34
|
+
* - `top` set → the panel's TOP edge is anchored, so it opens DOWNWARD.
|
|
35
|
+
* Native ignores the anchor and docks the panel to the bottom as a sheet.
|
|
32
36
|
*/
|
|
33
37
|
export interface ProfileMenuAnchor {
|
|
34
38
|
/** Distance from the viewport left, for the panel's LEFT edge. */
|
|
35
39
|
left: number;
|
|
36
40
|
/** Distance from the viewport bottom, for the panel's BOTTOM edge (opens upward). */
|
|
37
|
-
bottom
|
|
41
|
+
bottom?: number;
|
|
42
|
+
/** Distance from the viewport top, for the panel's TOP edge (opens downward). */
|
|
43
|
+
top?: number;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export interface ProfileMenuProps {
|
|
@@ -56,8 +62,9 @@ export interface ProfileMenuProps {
|
|
|
56
62
|
type ProfileMenuContentProps = Omit<ProfileMenuProps, 'open'>;
|
|
57
63
|
|
|
58
64
|
/**
|
|
59
|
-
* Clean device-account switcher, modeled on the inbox `AccountMenu`
|
|
60
|
-
*
|
|
65
|
+
* Clean device-account switcher, modeled on the inbox `AccountMenu` and styled
|
|
66
|
+
* with react-native `StyleSheet` + the Bloom theme (via `useTheme`) so it
|
|
67
|
+
* renders in EVERY consumer regardless of NativeWind. Lists every account
|
|
61
68
|
* signed in on this device (from {@link useDeviceAccounts}); tapping a row
|
|
62
69
|
* switches, and each inactive row carries a sign-out icon. Below the list:
|
|
63
70
|
* Add account, Manage account, optional View profile, and Sign out of all.
|
|
@@ -170,8 +177,8 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
170
177
|
return (
|
|
171
178
|
<>
|
|
172
179
|
<ScrollView
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
style={styles.scroll}
|
|
181
|
+
contentContainerStyle={styles.scrollContent}
|
|
175
182
|
showsVerticalScrollIndicator={false}
|
|
176
183
|
>
|
|
177
184
|
{/* 1) Device accounts — active first (checkmark), others switchable. */}
|
|
@@ -187,9 +194,11 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
187
194
|
accessibilityState={{ selected: isActive }}
|
|
188
195
|
onPress={() => handleSwitch(account.sessionId)}
|
|
189
196
|
disabled={isActive || isBusy || isSwitching}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
style={[
|
|
198
|
+
styles.accountRow,
|
|
199
|
+
isActive && { backgroundColor: colors.backgroundSecondary },
|
|
200
|
+
isSwitching && !isActive && styles.rowDimmed,
|
|
201
|
+
]}
|
|
193
202
|
>
|
|
194
203
|
<Avatar
|
|
195
204
|
source={account.user.avatar ?? undefined}
|
|
@@ -198,16 +207,19 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
198
207
|
name={account.displayName}
|
|
199
208
|
size={isActive ? 40 : 32}
|
|
200
209
|
/>
|
|
201
|
-
<View
|
|
210
|
+
<View style={styles.accountInfo}>
|
|
202
211
|
<Text
|
|
203
|
-
|
|
212
|
+
style={[
|
|
213
|
+
isActive ? styles.accountNameActive : styles.accountName,
|
|
214
|
+
{ color: colors.text },
|
|
215
|
+
]}
|
|
204
216
|
numberOfLines={1}
|
|
205
217
|
>
|
|
206
218
|
{account.displayName}
|
|
207
219
|
</Text>
|
|
208
220
|
{account.email ? (
|
|
209
221
|
<Text
|
|
210
|
-
|
|
222
|
+
style={[styles.accountEmail, { color: colors.textSecondary }]}
|
|
211
223
|
numberOfLines={1}
|
|
212
224
|
>
|
|
213
225
|
{account.email}
|
|
@@ -230,7 +242,7 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
230
242
|
onPress={() => handleRemove(account.sessionId)}
|
|
231
243
|
disabled={actionDisabled}
|
|
232
244
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
233
|
-
|
|
245
|
+
style={styles.rowSignOutButton}
|
|
234
246
|
>
|
|
235
247
|
<MaterialCommunityIcons
|
|
236
248
|
name="logout"
|
|
@@ -245,9 +257,9 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
245
257
|
|
|
246
258
|
{/* 2) Switching indicator. */}
|
|
247
259
|
{isSwitching ? (
|
|
248
|
-
<View
|
|
260
|
+
<View style={styles.switchingRow}>
|
|
249
261
|
<ActivityIndicator color={colors.textSecondary} size="small" />
|
|
250
|
-
<Text
|
|
262
|
+
<Text style={[styles.switchingText, { color: colors.textSecondary }]}>
|
|
251
263
|
{t('accountMenu.switching') || 'Switching account…'}
|
|
252
264
|
</Text>
|
|
253
265
|
</View>
|
|
@@ -259,6 +271,7 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
259
271
|
<ActionRow
|
|
260
272
|
icon="account-plus-outline"
|
|
261
273
|
iconColor={colors.icon}
|
|
274
|
+
textColor={colors.text}
|
|
262
275
|
label={t('accountMenu.addAnother') || 'Add another account'}
|
|
263
276
|
disabled={actionDisabled}
|
|
264
277
|
onPress={() => {
|
|
@@ -271,6 +284,7 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
271
284
|
<ActionRow
|
|
272
285
|
icon="cog-outline"
|
|
273
286
|
iconColor={colors.icon}
|
|
287
|
+
textColor={colors.text}
|
|
274
288
|
label={t('accountMenu.manage') || 'Manage your Oxy Account'}
|
|
275
289
|
disabled={actionDisabled}
|
|
276
290
|
onPress={() => {
|
|
@@ -284,6 +298,7 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
284
298
|
<ActionRow
|
|
285
299
|
icon="account-outline"
|
|
286
300
|
iconColor={colors.icon}
|
|
301
|
+
textColor={colors.text}
|
|
287
302
|
label={t('accountMenu.viewProfile') || 'View profile'}
|
|
288
303
|
disabled={actionDisabled}
|
|
289
304
|
onPress={() => {
|
|
@@ -300,14 +315,14 @@ const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
|
300
315
|
accessibilityLabel={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
|
|
301
316
|
onPress={() => signOutAllDialog.open()}
|
|
302
317
|
disabled={actionDisabled}
|
|
303
|
-
|
|
318
|
+
style={[styles.signOutAllRow, actionDisabled && styles.rowDimmed]}
|
|
304
319
|
>
|
|
305
320
|
{signingOutAll ? (
|
|
306
321
|
<ActivityIndicator color={colors.error} size="small" />
|
|
307
322
|
) : (
|
|
308
323
|
<MaterialCommunityIcons name="logout" size={18} color={colors.error} />
|
|
309
324
|
)}
|
|
310
|
-
<Text
|
|
325
|
+
<Text style={[styles.actionText, { color: colors.error }]}>
|
|
311
326
|
{t('accountMenu.signOutAll') || 'Sign out of all accounts'}
|
|
312
327
|
</Text>
|
|
313
328
|
</Pressable>
|
|
@@ -352,16 +367,21 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({
|
|
|
352
367
|
onBeforeSessionChange,
|
|
353
368
|
}) => {
|
|
354
369
|
const { t } = useI18n();
|
|
370
|
+
const { colors } = useTheme();
|
|
355
371
|
|
|
356
|
-
// Web: anchor the panel
|
|
357
|
-
//
|
|
358
|
-
//
|
|
372
|
+
// Web: anchor the panel to the measured trigger. When the anchor pins `top`
|
|
373
|
+
// the panel opens DOWNWARD from the trigger; otherwise it pins `bottom` and
|
|
374
|
+
// opens UPWARD (the default footer behavior). With no anchor captured, fall
|
|
375
|
+
// back to a bottom-left placement. Native ignores this (the panel docks to
|
|
376
|
+
// the bottom via the overlay's flex-end).
|
|
359
377
|
const panelAnchorStyle: ViewStyle | undefined = isWeb
|
|
360
378
|
? {
|
|
361
379
|
position: 'absolute',
|
|
362
380
|
width: MENU_WIDTH,
|
|
363
381
|
left: anchor?.left ?? 8,
|
|
364
|
-
|
|
382
|
+
...(typeof anchor?.top === 'number'
|
|
383
|
+
? { top: anchor.top }
|
|
384
|
+
: { bottom: anchor?.bottom ?? 8 }),
|
|
365
385
|
}
|
|
366
386
|
: undefined;
|
|
367
387
|
|
|
@@ -376,19 +396,20 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({
|
|
|
376
396
|
accessibilityRole="button"
|
|
377
397
|
accessibilityLabel={t('common.actions.close') || 'Close'}
|
|
378
398
|
onPress={onClose}
|
|
379
|
-
|
|
380
|
-
style={!isWeb ? styles.nativeScrim : undefined}
|
|
399
|
+
style={isWeb ? styles.webOverlay : styles.nativeOverlay}
|
|
381
400
|
>
|
|
382
401
|
<Pressable
|
|
383
402
|
// Swallow taps inside the panel so they never reach the overlay's
|
|
384
403
|
// outside-tap-to-close handler.
|
|
385
404
|
onPress={() => undefined}
|
|
386
|
-
|
|
387
|
-
isWeb
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
405
|
+
style={[
|
|
406
|
+
isWeb ? styles.panelWeb : styles.panelNative,
|
|
407
|
+
{ backgroundColor: colors.background },
|
|
408
|
+
isWeb && { borderColor: colors.border },
|
|
409
|
+
panelAnchorStyle,
|
|
410
|
+
styles.panelBounds,
|
|
411
|
+
styles.shadow,
|
|
412
|
+
]}
|
|
392
413
|
accessibilityRole="menu"
|
|
393
414
|
accessibilityLabel={t('accountMenu.label') || 'Account menu'}
|
|
394
415
|
>
|
|
@@ -412,36 +433,136 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({
|
|
|
412
433
|
const ActionRow: React.FC<{
|
|
413
434
|
icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
|
|
414
435
|
iconColor: string;
|
|
436
|
+
textColor: string;
|
|
415
437
|
label: string;
|
|
416
438
|
disabled: boolean;
|
|
417
439
|
onPress: () => void;
|
|
418
|
-
}> = ({ icon, iconColor, label, disabled, onPress }) => (
|
|
440
|
+
}> = ({ icon, iconColor, textColor, label, disabled, onPress }) => (
|
|
419
441
|
<Pressable
|
|
420
442
|
accessibilityRole="menuitem"
|
|
421
443
|
accessibilityLabel={label}
|
|
422
444
|
onPress={onPress}
|
|
423
445
|
disabled={disabled}
|
|
424
|
-
|
|
446
|
+
style={[styles.actionRow, disabled && styles.rowDimmed]}
|
|
425
447
|
>
|
|
426
448
|
<MaterialCommunityIcons name={icon} size={20} color={iconColor} />
|
|
427
|
-
<Text
|
|
449
|
+
<Text style={[styles.actionText, { color: textColor }]}>{label}</Text>
|
|
428
450
|
</Pressable>
|
|
429
451
|
);
|
|
430
452
|
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
453
|
+
const styles = StyleSheet.create({
|
|
454
|
+
// Overlay (`flex-1 relative` web / `flex-1 justify-end` + scrim native).
|
|
455
|
+
webOverlay: {
|
|
456
|
+
flex: 1,
|
|
457
|
+
position: 'relative',
|
|
458
|
+
},
|
|
459
|
+
nativeOverlay: {
|
|
460
|
+
flex: 1,
|
|
461
|
+
justifyContent: 'flex-end',
|
|
462
|
+
backgroundColor: 'rgba(0,0,0,0.32)',
|
|
463
|
+
},
|
|
464
|
+
// Panel shell — web popover (`overflow-hidden rounded-2xl border`).
|
|
465
|
+
panelWeb: {
|
|
466
|
+
overflow: 'hidden',
|
|
467
|
+
borderRadius: 16,
|
|
468
|
+
borderWidth: 1,
|
|
469
|
+
},
|
|
470
|
+
// Panel shell — native bottom sheet (`overflow-hidden rounded-t-3xl pb-3`).
|
|
471
|
+
panelNative: {
|
|
472
|
+
overflow: 'hidden',
|
|
473
|
+
borderTopLeftRadius: 24,
|
|
474
|
+
borderTopRightRadius: 24,
|
|
475
|
+
paddingBottom: 12,
|
|
476
|
+
},
|
|
477
|
+
// Shared `maxHeight: '85%'` cap applied to both panel variants.
|
|
478
|
+
panelBounds: {
|
|
479
|
+
maxHeight: '85%',
|
|
480
|
+
},
|
|
481
|
+
// The panel's drop shadow — dynamic elevation with no class equivalent.
|
|
435
482
|
shadow: {
|
|
436
483
|
shadowColor: '#000',
|
|
437
484
|
shadowOpacity: 0.18,
|
|
438
485
|
shadowRadius: 24,
|
|
439
486
|
shadowOffset: { width: 0, height: 8 },
|
|
440
487
|
elevation: 12,
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
488
|
+
},
|
|
489
|
+
// Scroll region (`grow-0` + `py-1` content).
|
|
490
|
+
scroll: {
|
|
491
|
+
flexGrow: 0,
|
|
492
|
+
},
|
|
493
|
+
scrollContent: {
|
|
494
|
+
paddingVertical: 4,
|
|
495
|
+
},
|
|
496
|
+
// Account row (`flex-row items-center gap-3 px-4 py-2.5`).
|
|
497
|
+
accountRow: {
|
|
498
|
+
flexDirection: 'row',
|
|
499
|
+
alignItems: 'center',
|
|
500
|
+
gap: 12,
|
|
501
|
+
paddingHorizontal: 16,
|
|
502
|
+
paddingVertical: 10,
|
|
503
|
+
},
|
|
504
|
+
// `opacity-40` dim applied to disabled / non-active-while-switching rows.
|
|
505
|
+
rowDimmed: {
|
|
506
|
+
opacity: 0.4,
|
|
507
|
+
},
|
|
508
|
+
// Identity block (`min-w-0 flex-1`).
|
|
509
|
+
accountInfo: {
|
|
510
|
+
flex: 1,
|
|
511
|
+
minWidth: 0,
|
|
512
|
+
},
|
|
513
|
+
// Inactive account name (`font-medium`).
|
|
514
|
+
accountName: {
|
|
515
|
+
fontWeight: '500',
|
|
516
|
+
},
|
|
517
|
+
// Active account name (`font-semibold`).
|
|
518
|
+
accountNameActive: {
|
|
519
|
+
fontWeight: '600',
|
|
520
|
+
},
|
|
521
|
+
// Secondary email line (`text-xs`).
|
|
522
|
+
accountEmail: {
|
|
523
|
+
fontSize: 12,
|
|
524
|
+
},
|
|
525
|
+
// Per-row sign-out button (`h-7 w-7 items-center justify-center rounded-full opacity-60`).
|
|
526
|
+
rowSignOutButton: {
|
|
527
|
+
width: 28,
|
|
528
|
+
height: 28,
|
|
529
|
+
alignItems: 'center',
|
|
530
|
+
justifyContent: 'center',
|
|
531
|
+
borderRadius: 9999,
|
|
532
|
+
opacity: 0.6,
|
|
533
|
+
},
|
|
534
|
+
// Switching indicator (`flex-row items-center justify-center gap-2 py-2`).
|
|
535
|
+
switchingRow: {
|
|
536
|
+
flexDirection: 'row',
|
|
537
|
+
alignItems: 'center',
|
|
538
|
+
justifyContent: 'center',
|
|
539
|
+
gap: 8,
|
|
540
|
+
paddingVertical: 8,
|
|
541
|
+
},
|
|
542
|
+
switchingText: {
|
|
543
|
+
fontSize: 12,
|
|
544
|
+
fontWeight: '500',
|
|
545
|
+
},
|
|
546
|
+
// Bottom action rows (`flex-row items-center gap-3 px-4 py-3`).
|
|
547
|
+
actionRow: {
|
|
548
|
+
flexDirection: 'row',
|
|
549
|
+
alignItems: 'center',
|
|
550
|
+
gap: 12,
|
|
551
|
+
paddingHorizontal: 16,
|
|
552
|
+
paddingVertical: 12,
|
|
553
|
+
},
|
|
554
|
+
// Sign-out-of-all row shares the action-row geometry.
|
|
555
|
+
signOutAllRow: {
|
|
556
|
+
flexDirection: 'row',
|
|
557
|
+
alignItems: 'center',
|
|
558
|
+
gap: 12,
|
|
559
|
+
paddingHorizontal: 16,
|
|
560
|
+
paddingVertical: 12,
|
|
561
|
+
},
|
|
562
|
+
// Action / sign-out label (`font-medium`).
|
|
563
|
+
actionText: {
|
|
564
|
+
fontWeight: '500',
|
|
565
|
+
},
|
|
566
|
+
});
|
|
446
567
|
|
|
447
568
|
export default ProfileMenu;
|