@oxyhq/services 13.1.6 → 13.2.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 +26 -9
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +94 -69
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +26 -9
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +95 -70
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +12 -0
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +19 -10
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts +12 -0
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +19 -10
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/components/ProfileButton.tsx +38 -9
- package/src/ui/components/ProfileMenu.tsx +102 -77
|
@@ -26,15 +26,20 @@ const isWeb = Platform.OS === 'web';
|
|
|
26
26
|
const MENU_WIDTH = 300;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Web-only anchor. `ProfileButton` measures its trigger and anchors the panel
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Web-only anchor. `ProfileButton` measures its trigger and anchors the panel to
|
|
30
|
+
* one corner so the menu opens either UPWARD (footer trigger) or DOWNWARD (a
|
|
31
|
+
* trigger at the top of a sidebar). Exactly ONE vertical edge is pinned:
|
|
32
|
+
* - `bottom` set → the panel's BOTTOM edge is anchored, so it opens UPWARD.
|
|
33
|
+
* - `top` set → the panel's TOP edge is anchored, so it opens DOWNWARD.
|
|
34
|
+
* Native ignores the anchor and docks the panel to the bottom as a sheet.
|
|
32
35
|
*/
|
|
33
36
|
export interface ProfileMenuAnchor {
|
|
34
37
|
/** Distance from the viewport left, for the panel's LEFT edge. */
|
|
35
38
|
left: number;
|
|
36
39
|
/** Distance from the viewport bottom, for the panel's BOTTOM edge (opens upward). */
|
|
37
|
-
bottom
|
|
40
|
+
bottom?: number;
|
|
41
|
+
/** Distance from the viewport top, for the panel's TOP edge (opens downward). */
|
|
42
|
+
top?: number;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface ProfileMenuProps {
|
|
@@ -52,6 +57,9 @@ export interface ProfileMenuProps {
|
|
|
52
57
|
onBeforeSessionChange?: () => void | Promise<void>;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/** Everything `ProfileMenuContent` needs, minus the `open` flag (always open). */
|
|
61
|
+
type ProfileMenuContentProps = Omit<ProfileMenuProps, 'open'>;
|
|
62
|
+
|
|
55
63
|
/**
|
|
56
64
|
* Clean device-account switcher, modeled on the inbox `AccountMenu` but written
|
|
57
65
|
* fresh with NativeWind classNames + Bloom primitives. Lists every account
|
|
@@ -59,13 +67,13 @@ export interface ProfileMenuProps {
|
|
|
59
67
|
* switches, and each inactive row carries a sign-out icon. Below the list:
|
|
60
68
|
* Add account, Manage account, optional View profile, and Sign out of all.
|
|
61
69
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
70
|
+
* Heavy hooks (device accounts / refresh-all, Bloom dialog/toast controls, the
|
|
71
|
+
* escape-key listener) live here so they run ONLY while the menu is open — the
|
|
72
|
+
* outer {@link ProfileMenu} mounts this only when `open`. This component may
|
|
73
|
+
* therefore assume it is always open.
|
|
64
74
|
*/
|
|
65
|
-
const
|
|
66
|
-
open,
|
|
75
|
+
const ProfileMenuContent: React.FC<ProfileMenuContentProps> = ({
|
|
67
76
|
onClose,
|
|
68
|
-
anchor,
|
|
69
77
|
onNavigateManage,
|
|
70
78
|
onAddAccount,
|
|
71
79
|
onNavigateProfile,
|
|
@@ -148,9 +156,10 @@ const ProfileMenuBody: React.FC<ProfileMenuProps> = ({
|
|
|
148
156
|
}
|
|
149
157
|
}, [signingOutAll, logoutAll, t, onClose, onBeforeSessionChange]);
|
|
150
158
|
|
|
151
|
-
// Escape-to-close (web only).
|
|
159
|
+
// Escape-to-close (web only). This component only mounts while open, so the
|
|
160
|
+
// listener is attached exactly for the menu's lifetime.
|
|
152
161
|
useEffect(() => {
|
|
153
|
-
if (!
|
|
162
|
+
if (!isWeb || typeof document === 'undefined') {
|
|
154
163
|
return undefined;
|
|
155
164
|
}
|
|
156
165
|
const onKey = (event: KeyboardEvent) => {
|
|
@@ -161,42 +170,10 @@ const ProfileMenuBody: React.FC<ProfileMenuProps> = ({
|
|
|
161
170
|
};
|
|
162
171
|
document.addEventListener('keydown', onKey, true);
|
|
163
172
|
return () => document.removeEventListener('keydown', onKey, true);
|
|
164
|
-
}, [
|
|
165
|
-
|
|
166
|
-
if (!open) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Web: anchor the panel's bottom-left corner to the measured trigger, falling
|
|
171
|
-
// back to a bottom-left placement when no anchor was captured. Native ignores
|
|
172
|
-
// this (the panel docks to the bottom via the overlay's flex-end).
|
|
173
|
-
const panelAnchorStyle: ViewStyle | undefined = isWeb
|
|
174
|
-
? {
|
|
175
|
-
position: 'absolute',
|
|
176
|
-
width: MENU_WIDTH,
|
|
177
|
-
left: anchor?.left ?? 8,
|
|
178
|
-
bottom: anchor?.bottom ?? 8,
|
|
179
|
-
}
|
|
180
|
-
: undefined;
|
|
173
|
+
}, [onClose]);
|
|
181
174
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Swallow taps inside the panel so they never reach the overlay's
|
|
185
|
-
// outside-tap-to-close handler.
|
|
186
|
-
onPress={() => undefined}
|
|
187
|
-
className={
|
|
188
|
-
isWeb
|
|
189
|
-
? 'overflow-hidden rounded-2xl border border-border bg-background'
|
|
190
|
-
: 'overflow-hidden rounded-t-3xl bg-background pb-3'
|
|
191
|
-
}
|
|
192
|
-
style={[
|
|
193
|
-
panelAnchorStyle,
|
|
194
|
-
!isWeb ? { maxHeight: '85%' } : { maxHeight: '85%' },
|
|
195
|
-
styles.shadow,
|
|
196
|
-
]}
|
|
197
|
-
accessibilityRole="menu"
|
|
198
|
-
accessibilityLabel={t('accountMenu.label') || 'Account menu'}
|
|
199
|
-
>
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
200
177
|
<ScrollView
|
|
201
178
|
className="grow-0"
|
|
202
179
|
contentContainerClassName="py-1"
|
|
@@ -340,8 +317,62 @@ const ProfileMenuBody: React.FC<ProfileMenuProps> = ({
|
|
|
340
317
|
</Text>
|
|
341
318
|
</Pressable>
|
|
342
319
|
</ScrollView>
|
|
343
|
-
|
|
320
|
+
|
|
321
|
+
<Dialog
|
|
322
|
+
control={signOutAllDialog}
|
|
323
|
+
title={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
|
|
324
|
+
description={t('common.confirms.signOutAll') || 'Are you sure you want to sign out of all accounts?'}
|
|
325
|
+
actions={[
|
|
326
|
+
{
|
|
327
|
+
label: t('accountMenu.signOutAll') || 'Sign out of all accounts',
|
|
328
|
+
color: 'destructive',
|
|
329
|
+
onPress: performSignOutAll,
|
|
330
|
+
},
|
|
331
|
+
{ label: t('common.cancel') || 'Cancel', color: 'cancel' },
|
|
332
|
+
]}
|
|
333
|
+
/>
|
|
334
|
+
</>
|
|
344
335
|
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Public wrapper. The RN `Modal` shell is ALWAYS mounted (only `visible` toggles)
|
|
340
|
+
* so the react-native-web portal is created up front — mounting a web `Modal`
|
|
341
|
+
* already-`visible` is fragile (the first interaction can be eaten / the button
|
|
342
|
+
* "does nothing"). Only the heavy {@link ProfileMenuContent} mounts when open,
|
|
343
|
+
* keeping the account hooks off the render path when the menu is closed. On
|
|
344
|
+
* native that closed render is `<Modal visible={false}>{null}</Modal>` → no heavy
|
|
345
|
+
* hooks run (preserving the native-drawer crash fix).
|
|
346
|
+
*
|
|
347
|
+
* This outer component calls ONLY light, always-safe hooks (`useTheme`,
|
|
348
|
+
* `useI18n` for the overlay a11y label) and computes the panel/overlay layout.
|
|
349
|
+
*/
|
|
350
|
+
const ProfileMenu: React.FC<ProfileMenuProps> = ({
|
|
351
|
+
open,
|
|
352
|
+
onClose,
|
|
353
|
+
anchor,
|
|
354
|
+
onNavigateManage,
|
|
355
|
+
onAddAccount,
|
|
356
|
+
onNavigateProfile,
|
|
357
|
+
onBeforeSessionChange,
|
|
358
|
+
}) => {
|
|
359
|
+
const { t } = useI18n();
|
|
360
|
+
|
|
361
|
+
// Web: anchor the panel to the measured trigger. When the anchor pins `top`
|
|
362
|
+
// the panel opens DOWNWARD from the trigger; otherwise it pins `bottom` and
|
|
363
|
+
// opens UPWARD (the default footer behavior). With no anchor captured, fall
|
|
364
|
+
// back to a bottom-left placement. Native ignores this (the panel docks to
|
|
365
|
+
// the bottom via the overlay's flex-end).
|
|
366
|
+
const panelAnchorStyle: ViewStyle | undefined = isWeb
|
|
367
|
+
? {
|
|
368
|
+
position: 'absolute',
|
|
369
|
+
width: MENU_WIDTH,
|
|
370
|
+
left: anchor?.left ?? 8,
|
|
371
|
+
...(typeof anchor?.top === 'number'
|
|
372
|
+
? { top: anchor.top }
|
|
373
|
+
: { bottom: anchor?.bottom ?? 8 }),
|
|
374
|
+
}
|
|
375
|
+
: undefined;
|
|
345
376
|
|
|
346
377
|
return (
|
|
347
378
|
<Modal
|
|
@@ -357,22 +388,31 @@ const ProfileMenuBody: React.FC<ProfileMenuProps> = ({
|
|
|
357
388
|
className={isWeb ? 'flex-1 relative' : 'flex-1 justify-end'}
|
|
358
389
|
style={!isWeb ? styles.nativeScrim : undefined}
|
|
359
390
|
>
|
|
360
|
-
|
|
391
|
+
<Pressable
|
|
392
|
+
// Swallow taps inside the panel so they never reach the overlay's
|
|
393
|
+
// outside-tap-to-close handler.
|
|
394
|
+
onPress={() => undefined}
|
|
395
|
+
className={
|
|
396
|
+
isWeb
|
|
397
|
+
? 'overflow-hidden rounded-2xl border border-border bg-background'
|
|
398
|
+
: 'overflow-hidden rounded-t-3xl bg-background pb-3'
|
|
399
|
+
}
|
|
400
|
+
style={[panelAnchorStyle, { maxHeight: '85%' }, styles.shadow]}
|
|
401
|
+
accessibilityRole="menu"
|
|
402
|
+
accessibilityLabel={t('accountMenu.label') || 'Account menu'}
|
|
403
|
+
>
|
|
404
|
+
{open ? (
|
|
405
|
+
<ProfileMenuContent
|
|
406
|
+
onClose={onClose}
|
|
407
|
+
anchor={anchor}
|
|
408
|
+
onNavigateManage={onNavigateManage}
|
|
409
|
+
onAddAccount={onAddAccount}
|
|
410
|
+
onNavigateProfile={onNavigateProfile}
|
|
411
|
+
onBeforeSessionChange={onBeforeSessionChange}
|
|
412
|
+
/>
|
|
413
|
+
) : null}
|
|
414
|
+
</Pressable>
|
|
361
415
|
</Pressable>
|
|
362
|
-
|
|
363
|
-
<Dialog
|
|
364
|
-
control={signOutAllDialog}
|
|
365
|
-
title={t('accountMenu.signOutAll') || 'Sign out of all accounts'}
|
|
366
|
-
description={t('common.confirms.signOutAll') || 'Are you sure you want to sign out of all accounts?'}
|
|
367
|
-
actions={[
|
|
368
|
-
{
|
|
369
|
-
label: t('accountMenu.signOutAll') || 'Sign out of all accounts',
|
|
370
|
-
color: 'destructive',
|
|
371
|
-
onPress: performSignOutAll,
|
|
372
|
-
},
|
|
373
|
-
{ label: t('common.cancel') || 'Cancel', color: 'cancel' },
|
|
374
|
-
]}
|
|
375
|
-
/>
|
|
376
416
|
</Modal>
|
|
377
417
|
);
|
|
378
418
|
};
|
|
@@ -413,19 +453,4 @@ const styles = {
|
|
|
413
453
|
} satisfies ViewStyle,
|
|
414
454
|
};
|
|
415
455
|
|
|
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
|
-
|
|
431
456
|
export default ProfileMenu;
|