feat(core): improve mobile perf (#15317)

#### PR Dependency Tree


* **PR #15317** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Virtualized mobile navigation with shell navigation and interactive
swipe menus; coordinated mobile back handling with interactive
phases/state restoration.
* Added shared auth request proxy and message-port based token handling
across mobile and worker flows.
* **Bug Fixes**
  * Hydrated remote worker error stacks for calls and observable errors.
* Improved SQLite FTS/indexer and nbstore optional text handling;
refined docs-search ref parsing and notification loading/retry.
* **Refactor / UX**
* Modal focus-preservation and pointer behavior updates; improved mobile
menu controls and back gesture plugins.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-23 00:23:21 +08:00
committed by GitHub
parent 02e75862cc
commit 1d36e2e4b2
160 changed files with 6660 additions and 1890 deletions
@@ -1,12 +1,11 @@
import type {
DropdownMenuContentProps,
DropdownMenuItemProps as MenuItemPropsPrimitive,
DropdownMenuPortalProps,
DropdownMenuProps,
DropdownMenuSubContentProps,
DropdownMenuSubProps,
} from '@radix-ui/react-dropdown-menu';
import type { CSSProperties, ReactNode } from 'react';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
export interface MenuRef {
changeOpen: (open: boolean) => void;
@@ -25,9 +24,12 @@ export interface MenuProps {
}
export interface MenuItemProps extends Omit<
MenuItemPropsPrimitive,
'asChild' | 'textValue' | 'prefix'
HTMLAttributes<HTMLElement>,
'onSelect' | 'prefix'
> {
disabled?: boolean;
textValue?: string;
onSelect?: (event: Event) => void;
type?: 'default' | 'warning' | 'danger';
prefix?: ReactNode;
suffix?: ReactNode;
@@ -1,47 +1,48 @@
import { useCallback, useContext } from 'react';
import { type MouseEvent, useCallback, useContext } from 'react';
import type { MenuItemProps } from '../menu.types';
import { useMenuItem } from '../use-menu-item';
import { MobileMenuContext } from './context';
let preventDefaultFlag = false;
const preventDefault = () => {
preventDefaultFlag = true;
};
export const MobileMenuItem = (props: MenuItemProps) => {
const { setOpen, subMenus, setSubMenus } = useContext(MobileMenuContext);
const { className, children, otherProps } = useMenuItem(props);
const { onSelect, onClick, divide, ...restProps } = otherProps;
const {
onSelect,
onClick,
divide,
textValue: _textValue,
...restProps
} = otherProps;
const onItemClick = useCallback(
(e: any) => {
onSelect?.(e);
onClick?.({ ...e, preventDefault });
if (preventDefaultFlag) {
preventDefaultFlag = false;
(event: MouseEvent<HTMLButtonElement>) => {
onSelect?.(event.nativeEvent);
onClick?.(event);
if (event.defaultPrevented || event.nativeEvent.defaultPrevented) {
return;
}
if (subMenus.length > 1) {
// assume the user can only click the last menu
// (mimic the back button)
setSubMenus(subMenus.slice(0, -1));
} else {
if (subMenus.length > 1) {
// assume the user can only click the last menu
// (mimic the back button)
setSubMenus(subMenus.slice(0, -1));
} else {
setOpen?.(false);
}
setOpen?.(false);
}
},
[onClick, onSelect, setOpen, setSubMenus, subMenus]
);
return (
<div
role="menuitem"
<button
type="button"
onClick={onItemClick}
className={className}
disabled={props.disabled}
data-divider={divide}
{...restProps}
>
{children}
</div>
</button>
);
};
@@ -5,8 +5,8 @@ import clsx from 'clsx';
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useState,
} from 'react';
@@ -71,13 +71,15 @@ export const MobileMenu = ({
const activeIndex = subMenus.length;
// dynamic height for slider
useEffect(() => {
useLayoutEffect(() => {
if (sliderElement && finalOpen) {
const active = sliderElement.querySelector(
`.${styles.menuContent}[data-index="${activeIndex}"]`
);
if (!active) return;
setSliderHeight(active.getBoundingClientRect().height);
// for the situation that content is loaded asynchronously
return observeResize(active, entry => {
setSliderHeight(entry.borderBoxSize[0].blockSize);
@@ -156,6 +158,7 @@ export const MobileMenu = ({
onOpenChange={onOpenChange}
width="100%"
animation="slideBottom"
contentAnimation="none"
withoutCloseButton={true}
contentOptions={{
className: clsx(className, styles.mobileMenuModal),
@@ -163,6 +166,7 @@ export const MobileMenu = ({
}}
contentWrapperStyle={contentWrapperStyle}
disableAutoFocus={true}
preserveEditingFocusOnAction
>
<div
ref={setSliderElement}
@@ -25,7 +25,7 @@ export const mobileMenuModal = style({
export const slider = style({
display: 'flex',
alignItems: 'start',
transition: 'all 0.23s',
transition: 'transform 0.23s',
});
export const menuContent = style({
@@ -42,8 +42,13 @@ export const menuContent = style({
});
export const mobileMenuItem = style({
width: '100%',
padding: '10px 20px',
borderRadius: 0,
appearance: 'none',
font: 'inherit',
lineHeight: '22px',
touchAction: 'manipulation',
':hover': {
vars: {
@@ -21,7 +21,7 @@ export const MobileMenuSub = ({
const {
className,
children,
otherProps: { onClick, ...otherTriggerOptions },
otherProps: { onClick, textValue: _textValue, ...otherTriggerOptions },
} = useMenuItem({
children: propsChildren,
suffixIcon: <ArrowRightSmallPlusIcon />,
@@ -35,9 +35,14 @@ export const MobileMenuSub = ({
subContentOptions={contentOptions}
title={title}
>
<div role="menuitem" className={className} {...otherTriggerOptions}>
<button
type="button"
className={className}
disabled={triggerOptions?.disabled}
{...otherTriggerOptions}
>
{children}
</div>
</button>
</MobileMenuSubRaw>
);
};
@@ -50,7 +55,7 @@ export const MobileMenuSubRaw = ({
subOptions,
subContentOptions,
}: MenuSubProps & {
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
onClick?: (e: MouseEvent<HTMLElement>) => void;
title?: string;
}) => {
const contentOptions = subContentOptions ?? EMPTY_SUB_CONTENT_OPTIONS;
@@ -67,7 +72,7 @@ export const MobileMenuSubRaw = ({
}, [addSubMenu, subMenuContent]);
const onItemClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
(e: MouseEvent<HTMLElement>) => {
onClick?.(e);
doAddSubMenu();
},
@@ -6,7 +6,7 @@ export interface ModalConfig {
* add global callback for modal open,
* return a function to handle close/unmount callback
*/
onOpen?: () => OnClose;
onOpen?: (close: () => void) => OnClose;
/**
* For mobile
*/
@@ -25,6 +25,8 @@ import { SafeArea } from '../safe-area';
import { InsideModalContext, ModalConfigContext } from './context';
import * as styles from './styles.css';
type ModalAnimation = 'fadeScaleTop' | 'none' | 'slideBottom' | 'slideRight';
export interface ModalProps extends DialogProps {
width?: CSSProperties['width'];
height?: CSSProperties['height'];
@@ -48,17 +50,29 @@ export interface ModalProps extends DialogProps {
/**
* @default 'fadeScaleTop'
*/
animation?: 'fadeScaleTop' | 'none' | 'slideBottom' | 'slideRight';
animation?: ModalAnimation;
contentAnimation?: ModalAnimation;
/**
* Whether to show the modal in full screen mode
*/
fullScreen?: boolean;
disableAutoFocus?: boolean;
preserveEditingFocusOnAction?: boolean;
}
type PointerDownOutsideEvent = Parameters<
Exclude<DialogContentProps['onPointerDownOutside'], undefined>
>[0];
const isTextEditingElement = (element: Element) =>
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
(element instanceof HTMLElement && element.isContentEditable);
const getEnabledModalAction = (element: Element) =>
element.closest(
'button:not(:disabled), [role="button"]:not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"]), [data-modal-action]:not([aria-disabled="true"])'
);
const getVar = (style: number | string = '', defaultValue = '') => {
return style
? typeof style === 'number'
@@ -151,6 +165,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
style: contentStyle,
className: contentClassName,
onPointerDownOutside,
onPointerDown,
onEscapeKeyDown,
...otherContentOptions
} = {},
@@ -164,8 +179,10 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
contentWrapperClassName,
contentWrapperStyle,
animation = BUILD_CONFIG.isMobileEdition ? 'slideBottom' : 'fadeScaleTop',
contentAnimation = animation,
fullScreen,
disableAutoFocus,
preserveEditingFocusOnAction = false,
...otherProps
} = props;
const { className: closeButtonClassName, ...otherCloseButtonProps } =
@@ -176,9 +193,9 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
);
useEffect(() => {
if (open) return modalConfigOnOpen?.();
if (open) return modalConfigOnOpen?.(() => onOpenChange?.(false));
return;
}, [modalConfigOnOpen, open]);
}, [modalConfigOnOpen, onOpenChange, open]);
useEffect(() => {
if (open) {
@@ -211,6 +228,29 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
[onEscapeKeyDown, persistent]
);
const handlePointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
onPointerDown?.(event);
if (
!preserveEditingFocusOnAction ||
event.defaultPrevented ||
!(event.target instanceof Element)
) {
return;
}
const activeElement = document.activeElement;
if (
activeElement &&
event.currentTarget.contains(activeElement) &&
isTextEditingElement(activeElement) &&
getEnabledModalAction(event.target)
) {
event.preventDefault();
}
},
[onPointerDown, preserveEditingFocusOnAction]
);
const handleAutoFocus = useCallback(
(e: Event) => {
disableAutoFocus && e.preventDefault();
@@ -244,19 +284,34 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
>
<SafeArea
bottom={BUILD_CONFIG.isMobileEdition}
bottomOffset={dynamicKeyboardHeight ?? 12}
bottomOffset={
fullScreen
? 0
: dynamicKeyboardHeight === undefined
? 12
: `calc(${getVar(dynamicKeyboardHeight)} + 12px)`
}
data-full-screen={fullScreen}
data-modal={modal}
className={clsx(
`anim-${animation}`,
`anim-${contentAnimation}`,
styles.modalContentWrapper,
contentWrapperClassName
)}
data-mobile={BUILD_CONFIG.isMobileEdition ? '' : undefined}
style={contentWrapperStyle}
style={{
...assignInlineVars({
[styles.keyboardInsetVar]: getVar(
dynamicKeyboardHeight,
'0px'
),
}),
...contentWrapperStyle,
}}
>
<Dialog.Content
onPointerDownOutside={handlePointerDownOutSide}
onPointerDown={handlePointerDown}
onEscapeKeyDown={handleEscapeKeyDown}
className={clsx(styles.modalContent, contentClassName)}
onOpenAutoFocus={handleAutoFocus}
@@ -268,9 +323,13 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
),
[styles.heightVar]: getVar(
height,
fullScreen ? '100dvh' : 'unset'
fullScreen
? `calc(100dvh - ${styles.keyboardInsetVar})`
: 'unset'
),
[styles.minHeightVar]: getVar(minHeight, '26px'),
[styles.minHeightVar]: fullScreen
? `calc(100dvh - ${styles.keyboardInsetVar})`
: getVar(minHeight, '26px'),
}),
...contentStyle,
}}
@@ -86,6 +86,7 @@ export const PromptModal = ({
return (
<Modal
preserveEditingFocusOnAction={BUILD_CONFIG.isMobileEdition}
contentOptions={{
className: styles.container,
onPointerDownOutside: e => {
@@ -12,6 +12,7 @@ import { vtScopeSelector } from '../../utils/view-transition';
export const widthVar = createVar('widthVar');
export const heightVar = createVar('heightVar');
export const minHeightVar = createVar('minHeightVar');
export const keyboardInsetVar = createVar('keyboardInsetVar');
export const modalVTScope = generateIdentifier('modal');
@@ -89,14 +90,24 @@ export const modalContentWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
zIndex: cssVar('zIndexModal'),
transition: 'padding-bottom 230ms ease-out',
'@media': {
'(prefers-reduced-motion: reduce)': {
transition: 'none',
},
},
selectors: {
'&[data-mobile]': {
alignItems: 'flex-end',
paddingBottom: 'env(safe-area-inset-bottom, 20px)',
paddingBottom:
'var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 20px))',
},
'&[data-full-screen="true"]': {
alignItems: 'flex-start',
padding: '0 !important',
},
'&.anim-none': {
@@ -174,8 +185,8 @@ export const modalContent = style({
'[data-full-screen="true"] &': {
vars: {
[widthVar]: '100dvw',
[heightVar]: '100dvh',
[minHeightVar]: '100dvh',
[heightVar]: `calc(100dvh - ${keyboardInsetVar})`,
[minHeightVar]: `calc(100dvh - ${keyboardInsetVar})`,
},
maxWidth: '100dvw',
maxHeight: '100dvh',
@@ -14,7 +14,7 @@ export function MobileNotificationCenter() {
position="top-center"
style={{
width: '100%',
top: 'calc(env(safe-area-inset-top) + 16px)',
top: 'calc(var(--safe-area-inset-top, env(safe-area-inset-top, 0px)) + 16px)',
pointerEvents: 'auto',
}}
theme={resolvedTheme}
@@ -12,10 +12,10 @@ export const safeArea = style({
paddingBottom: `calc(${fallbackVar(bottomOffsetVar, '0px')} + 0px)`,
},
'&[data-standalone][data-top]': {
paddingTop: `calc(env(safe-area-inset-top, 12px) + ${topOffsetVar})`,
paddingTop: `calc(var(--safe-area-inset-top, env(safe-area-inset-top, 12px)) + ${topOffsetVar})`,
},
'&[data-standalone][data-bottom]': {
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${fallbackVar(bottomOffsetVar, '0px')})`,
paddingBottom: `calc(var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)) + ${fallbackVar(bottomOffsetVar, '0px')})`,
},
},
});