feat(mobile): swipe to open menu for explorer (#8953)
close AF-1803 - bump theme - extract `SwipeHelper` and add `speed`, `direction` detection support - new mobile `SwipeMenu` component - integrate `SwipeMenu` to open a menu in Explorer - New `Haptics` module for mobile, implemented in `ios` and `mobile`(`navigator.vibrate()`) 
This commit is contained in:
@@ -13,6 +13,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorCordova', :path => '../../../../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../../../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../../../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../../../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorBrowser', :path => '../../../../../node_modules/@capacitor/browser'
|
pod 'CapacitorBrowser', :path => '../../../../../node_modules/@capacitor/browser'
|
||||||
|
pod 'CapacitorHaptics', :path => '../../../../../node_modules/@capacitor/haptics'
|
||||||
pod 'CapacitorKeyboard', :path => '../../../../../node_modules/@capacitor/keyboard'
|
pod 'CapacitorKeyboard', :path => '../../../../../node_modules/@capacitor/keyboard'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Capacitor (6.1.2):
|
- Capacitor (6.2.0):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorApp (6.0.1):
|
- CapacitorApp (6.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorBrowser (6.0.3):
|
- CapacitorBrowser (6.0.4):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCordova (6.1.2)
|
- CapacitorCordova (6.2.0)
|
||||||
- CapacitorKeyboard (6.0.2):
|
- CapacitorHaptics (6.0.2):
|
||||||
|
- Capacitor
|
||||||
|
- CapacitorKeyboard (6.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CryptoSwift (1.8.3)
|
- CryptoSwift (1.8.3)
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ DEPENDENCIES:
|
|||||||
- "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)"
|
- "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)"
|
||||||
- "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)"
|
- "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)"
|
||||||
- "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)"
|
- "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)"
|
||||||
|
- "CapacitorHaptics (from `../../../../../node_modules/@capacitor/haptics`)"
|
||||||
- "CapacitorKeyboard (from `../../../../../node_modules/@capacitor/keyboard`)"
|
- "CapacitorKeyboard (from `../../../../../node_modules/@capacitor/keyboard`)"
|
||||||
- CryptoSwift (~> 1.8.3)
|
- CryptoSwift (~> 1.8.3)
|
||||||
|
|
||||||
@@ -31,17 +34,20 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../../../node_modules/@capacitor/browser"
|
:path: "../../../../../node_modules/@capacitor/browser"
|
||||||
CapacitorCordova:
|
CapacitorCordova:
|
||||||
:path: "../../../../../node_modules/@capacitor/ios"
|
:path: "../../../../../node_modules/@capacitor/ios"
|
||||||
|
CapacitorHaptics:
|
||||||
|
:path: "../../../../../node_modules/@capacitor/haptics"
|
||||||
CapacitorKeyboard:
|
CapacitorKeyboard:
|
||||||
:path: "../../../../../node_modules/@capacitor/keyboard"
|
:path: "../../../../../node_modules/@capacitor/keyboard"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Capacitor: 679f9673fdf30597493a6362a5d5bf233d46abc2
|
Capacitor: 1f3c7b9802d958cd8c4eb63895fff85dff2e1eea
|
||||||
CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1
|
CapacitorApp: 2a8c3a0b0814322e5e6e15fe595f02c3808f0f8b
|
||||||
CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e
|
CapacitorBrowser: ef0529d16cd8839281050c350e7bbee4f5c6d65f
|
||||||
CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd
|
CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9
|
||||||
CapacitorKeyboard: 2700f9b18687be021e28b5a09b59eb151a46d5e0
|
CapacitorHaptics: b53409aaca1203f79c6d0eb3ed5de40556339518
|
||||||
|
CapacitorKeyboard: 460c6f9ec5e52c84f2742d5ce2e67bbc7ab0ebb0
|
||||||
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
|
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
|
||||||
|
|
||||||
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
|
PODFILE CHECKSUM: e0c0ccf027ea6d51e476f0baf9d44d97b9a90a4b
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@capacitor/app": "^6.0.1",
|
"@capacitor/app": "^6.0.1",
|
||||||
"@capacitor/browser": "^6.0.3",
|
"@capacitor/browser": "^6.0.3",
|
||||||
"@capacitor/core": "^6.1.2",
|
"@capacitor/core": "^6.1.2",
|
||||||
|
"@capacitor/haptics": "^6.0.2",
|
||||||
"@capacitor/ios": "^6.1.2",
|
"@capacitor/ios": "^6.1.2",
|
||||||
"@capacitor/keyboard": "^6.0.2",
|
"@capacitor/keyboard": "^6.0.2",
|
||||||
"@sentry/react": "^8.0.0",
|
"@sentry/react": "^8.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AffineContext } from '@affine/core/components/context';
|
import { AffineContext } from '@affine/core/components/context';
|
||||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||||
|
import { HapticProvider } from '@affine/core/mobile/modules/haptics';
|
||||||
import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigation-gesture';
|
import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigation-gesture';
|
||||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||||
import { router } from '@affine/core/mobile/router';
|
import { router } from '@affine/core/mobile/router';
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from '@affine/core/modules/workspace-engine';
|
} from '@affine/core/modules/workspace-engine';
|
||||||
import { App as CapacitorApp } from '@capacitor/app';
|
import { App as CapacitorApp } from '@capacitor/app';
|
||||||
import { Browser } from '@capacitor/browser';
|
import { Browser } from '@capacitor/browser';
|
||||||
|
import { Haptics } from '@capacitor/haptics';
|
||||||
import { Keyboard } from '@capacitor/keyboard';
|
import { Keyboard } from '@capacitor/keyboard';
|
||||||
import {
|
import {
|
||||||
Framework,
|
Framework,
|
||||||
@@ -94,6 +96,14 @@ framework.impl(NavigationGestureProvider, {
|
|||||||
enable: () => NavigationGesture.enable(),
|
enable: () => NavigationGesture.enable(),
|
||||||
disable: () => NavigationGesture.disable(),
|
disable: () => NavigationGesture.disable(),
|
||||||
});
|
});
|
||||||
|
framework.impl(HapticProvider, {
|
||||||
|
impact: options => Haptics.impact(options as any),
|
||||||
|
vibrate: options => Haptics.vibrate(options as any),
|
||||||
|
notification: options => Haptics.notification(options as any),
|
||||||
|
selectionStart: () => Haptics.selectionStart(),
|
||||||
|
selectionChanged: () => Haptics.selectionChanged(),
|
||||||
|
selectionEnd: () => Haptics.selectionEnd(),
|
||||||
|
});
|
||||||
const frameworkProvider = framework.provider();
|
const frameworkProvider = framework.provider();
|
||||||
|
|
||||||
// setup application lifecycle events, and emit application start event
|
// setup application lifecycle events, and emit application start event
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AffineContext } from '@affine/core/components/context';
|
import { AffineContext } from '@affine/core/components/context';
|
||||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||||
|
import { HapticProvider } from '@affine/core/mobile/modules/haptics';
|
||||||
import { router } from '@affine/core/mobile/router';
|
import { router } from '@affine/core/mobile/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||||
@@ -52,6 +53,28 @@ framework.impl(PopupWindowProvider, {
|
|||||||
window.open(url, '_blank', 'noreferrer noopener');
|
window.open(url, '_blank', 'noreferrer noopener');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
framework.impl(HapticProvider, {
|
||||||
|
impact: options => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const style = options?.style ?? 'LIGHT';
|
||||||
|
const pattern = {
|
||||||
|
LIGHT: [10],
|
||||||
|
MEDIUM: [20],
|
||||||
|
HEAVY: [30],
|
||||||
|
}[style];
|
||||||
|
const result = navigator.vibrate?.(pattern);
|
||||||
|
if (!result) {
|
||||||
|
console.warn('vibrate not supported, or user not interacted');
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
notification: () => Promise.reject('Not supported'),
|
||||||
|
vibrate: () => Promise.reject('Not supported'),
|
||||||
|
selectionStart: () => Promise.reject('Not supported'),
|
||||||
|
selectionChanged: () => Promise.reject('Not supported'),
|
||||||
|
selectionEnd: () => Promise.reject('Not supported'),
|
||||||
|
});
|
||||||
const frameworkProvider = framework.provider();
|
const frameworkProvider = framework.provider();
|
||||||
|
|
||||||
// setup application lifecycle events, and emit application start event
|
// setup application lifecycle events, and emit application start event
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
"@radix-ui/react-visually-hidden": "^1.1.0",
|
||||||
"@toeverything/theme": "^1.0.18",
|
"@toeverything/theme": "^1.0.21",
|
||||||
"@vanilla-extract/dynamic": "^2.1.0",
|
"@vanilla-extract/dynamic": "^2.1.0",
|
||||||
"check-password-strength": "^2.0.10",
|
"check-password-strength": "^2.0.10",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@radix-ui/react-toolbar": "^1.0.4",
|
"@radix-ui/react-toolbar": "^1.0.4",
|
||||||
"@sentry/react": "^8.0.0",
|
"@sentry/react": "^8.0.0",
|
||||||
"@toeverything/pdf-viewer": "^0.1.1",
|
"@toeverything/pdf-viewer": "^0.1.1",
|
||||||
"@toeverything/theme": "^1.0.18",
|
"@toeverything/theme": "^1.0.21",
|
||||||
"@vanilla-extract/dynamic": "^2.1.0",
|
"@vanilla-extract/dynamic": "^2.1.0",
|
||||||
"animejs": "^3.2.2",
|
"animejs": "^3.2.2",
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 99,
|
index: 97,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<LinkedPageIcon />}
|
prefixIcon={<LinkedPageIcon />}
|
||||||
@@ -244,7 +244,7 @@ export const useExplorerDocNodeOperationsMenu = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 99,
|
index: 98,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem prefixIcon={<DuplicateIcon />} onClick={handleDuplicate}>
|
<MenuItem prefixIcon={<DuplicateIcon />} onClick={handleDuplicate}>
|
||||||
{t['com.affine.header.option.duplicate']()}
|
{t['com.affine.header.option.duplicate']()}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { BaseExplorerTreeNodeProps } from '@affine/core/modules/explorer';
|
|||||||
import { ExplorerTreeContext } from '@affine/core/modules/explorer';
|
import { ExplorerTreeContext } from '@affine/core/modules/explorer';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import { extractEmojiIcon } from '@affine/core/utils';
|
import { extractEmojiIcon } from '@affine/core/utils';
|
||||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowDownSmallIcon, MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { SwipeMenu } from '../../swipe-menu';
|
||||||
import * as styles from './node.css';
|
import * as styles from './node.css';
|
||||||
|
|
||||||
interface ExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {}
|
interface ExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {}
|
||||||
@@ -43,6 +44,7 @@ export const ExplorerTreeNode = ({
|
|||||||
const clickForCollapse = !onClick && !to && !disabled;
|
const clickForCollapse = !onClick && !to && !disabled;
|
||||||
const [childCount, setChildCount] = useState(0);
|
const [childCount, setChildCount] = useState(0);
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
const { emoji, name } = useMemo(() => {
|
const { emoji, name } = useMemo(() => {
|
||||||
if (!extractEmojiAsIcon || !rawName) {
|
if (!extractEmojiAsIcon || !rawName) {
|
||||||
@@ -160,15 +162,32 @@ export const ExplorerTreeNode = ({
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.contentContainer} data-open={!collapsed}>
|
<SwipeMenu
|
||||||
{to ? (
|
onExecute={useCallback(() => setMenuOpen(true), [])}
|
||||||
<LinkComponent to={to} className={styles.linkItemRoot}>
|
menu={
|
||||||
{content}
|
<MobileMenu
|
||||||
</LinkComponent>
|
rootOptions={useMemo(
|
||||||
) : (
|
() => ({ open: menuOpen, onOpenChange: setMenuOpen }),
|
||||||
<div>{content}</div>
|
[menuOpen]
|
||||||
)}
|
)}
|
||||||
</div>
|
items={menuOperations.map(({ view, index }) => (
|
||||||
|
<Fragment key={index}>{view}</Fragment>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon fontSize={24} />
|
||||||
|
</MobileMenu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.contentContainer} data-open={!collapsed}>
|
||||||
|
{to ? (
|
||||||
|
<LinkComponent to={to} className={styles.linkItemRoot}>
|
||||||
|
{content}
|
||||||
|
</LinkComponent>
|
||||||
|
) : (
|
||||||
|
<div>{content}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SwipeMenu>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
{/* For lastInGroup check, the placeholder must be placed above all children in the dom */}
|
{/* For lastInGroup check, the placeholder must be placed above all children in the dom */}
|
||||||
<div className={styles.collapseContentPlaceholder}>
|
<div className={styles.collapseContentPlaceholder}>
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export * from './page-header';
|
|||||||
export * from './rename';
|
export * from './rename';
|
||||||
export * from './search-input';
|
export * from './search-input';
|
||||||
export * from './search-result';
|
export * from './search-result';
|
||||||
|
export * from './swipe-menu';
|
||||||
export * from './user-plan-tag';
|
export * from './user-plan-tag';
|
||||||
export * from './workspace-selector';
|
export * from './workspace-selector';
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import anime from 'animejs';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { HapticsService } from '../../modules/haptics';
|
||||||
|
import { SwipeHelper } from '../../utils';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface SwipeMenuProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
menu: ReactNode;
|
||||||
|
/**
|
||||||
|
* if the swipe distance is greater than the threshold, will execute the callback
|
||||||
|
* @default 200
|
||||||
|
*/
|
||||||
|
executeThreshold?: number;
|
||||||
|
onExecute?: () => void;
|
||||||
|
normalWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TickFunc<T extends Array<any> = any[]> = (
|
||||||
|
content: HTMLDivElement,
|
||||||
|
menu: HTMLDivElement,
|
||||||
|
options: {
|
||||||
|
deltaX: number;
|
||||||
|
normalWidth: number;
|
||||||
|
},
|
||||||
|
...args: T
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const tick: TickFunc = (content, menu, options) => {
|
||||||
|
const limitedDeltaX = Math.min(
|
||||||
|
0,
|
||||||
|
Math.max(options.deltaX, -options.normalWidth * 3)
|
||||||
|
);
|
||||||
|
content.style.transform = `translateX(${limitedDeltaX}px)`;
|
||||||
|
menu.style.transform = `translateX(${limitedDeltaX}px)`;
|
||||||
|
menu.style.width = `${Math.max(options.normalWidth, Math.abs(limitedDeltaX))}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const animate: TickFunc<[number]> = (content, menu, options, to) => {
|
||||||
|
const styleX = Number(content.style.transform.match(/-?\d+/)?.[0]);
|
||||||
|
const deltaX = isNaN(styleX) ? options.deltaX : styleX;
|
||||||
|
|
||||||
|
const proxy = new Proxy(
|
||||||
|
{ deltaX },
|
||||||
|
{
|
||||||
|
set(target, key, value) {
|
||||||
|
if (key !== 'deltaX') return false;
|
||||||
|
target.deltaX = value;
|
||||||
|
tick(content, menu, { ...options, deltaX: value });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (deltaX === to) return;
|
||||||
|
anime({
|
||||||
|
targets: proxy,
|
||||||
|
deltaX: to,
|
||||||
|
duration: 230,
|
||||||
|
easing: 'easeInOutSine',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeId$ = new LiveData<string | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only support swipe left yet
|
||||||
|
* Only support single menu item yet
|
||||||
|
*/
|
||||||
|
export const SwipeMenu = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
menu,
|
||||||
|
normalWidth = 90,
|
||||||
|
executeThreshold = 200,
|
||||||
|
onExecute,
|
||||||
|
...props
|
||||||
|
}: SwipeMenuProps) => {
|
||||||
|
const id = useId();
|
||||||
|
const haptics = useService(HapticsService);
|
||||||
|
const activeId = useLiveData(activeId$);
|
||||||
|
|
||||||
|
const isOpenRef = useRef(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
isOpenRef.current = false;
|
||||||
|
const content = contentRef.current;
|
||||||
|
const menu = menuRef.current;
|
||||||
|
if (!content || !menu) return;
|
||||||
|
animate(content, menu, { deltaX: -normalWidth, normalWidth }, 0);
|
||||||
|
}, [normalWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeId && activeId !== id && isOpenRef.current) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}, [activeId, handleClose, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const menu = menuRef.current;
|
||||||
|
const content = contentRef.current;
|
||||||
|
if (!container || !menu || !content) return;
|
||||||
|
const swipeHelper = new SwipeHelper();
|
||||||
|
|
||||||
|
let shouldExecute = false;
|
||||||
|
|
||||||
|
return swipeHelper.init(container, {
|
||||||
|
preventScroll: true,
|
||||||
|
direction: 'horizontal',
|
||||||
|
onSwipeStart() {
|
||||||
|
activeId$.next(id);
|
||||||
|
},
|
||||||
|
onSwipe({ deltaX: dragX, initialDirection }) {
|
||||||
|
if (initialDirection !== 'horizontal') return;
|
||||||
|
|
||||||
|
const deltaX = isOpenRef.current ? dragX - normalWidth : dragX;
|
||||||
|
const prevShouldExecute = shouldExecute;
|
||||||
|
shouldExecute = Math.abs(deltaX) > executeThreshold;
|
||||||
|
if (shouldExecute && !prevShouldExecute) {
|
||||||
|
haptics.impact({ style: 'LIGHT' });
|
||||||
|
}
|
||||||
|
tick(content, menu, { deltaX, normalWidth });
|
||||||
|
},
|
||||||
|
onSwipeEnd({ deltaX: dragX, initialDirection }) {
|
||||||
|
activeId$.next(null);
|
||||||
|
if (initialDirection !== 'horizontal') return;
|
||||||
|
|
||||||
|
const deltaX = isOpenRef.current ? dragX - normalWidth : dragX;
|
||||||
|
if (shouldExecute) {
|
||||||
|
animate(content, menu, { deltaX, normalWidth }, 0);
|
||||||
|
onExecute?.();
|
||||||
|
isOpenRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// open
|
||||||
|
if (Math.abs(deltaX) > normalWidth / 2) {
|
||||||
|
animate(content, menu, { deltaX, normalWidth }, -normalWidth);
|
||||||
|
isOpenRef.current = true;
|
||||||
|
}
|
||||||
|
// close
|
||||||
|
else {
|
||||||
|
animate(content, menu, { deltaX, normalWidth }, 0);
|
||||||
|
isOpenRef.current = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [executeThreshold, haptics, id, normalWidth, onExecute]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.container, className)}
|
||||||
|
ref={containerRef}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={styles.content} ref={contentRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu} ref={menuRef} onClick={handleClose}>
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
export const content = style({
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
});
|
||||||
|
export const menu = style({
|
||||||
|
position: 'absolute',
|
||||||
|
left: '100%',
|
||||||
|
top: 0,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
background: cssVarV2('layer/background/mobile/tertiary'),
|
||||||
|
});
|
||||||
13
packages/frontend/core/src/mobile/modules/haptics/index.ts
Normal file
13
packages/frontend/core/src/mobile/modules/haptics/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { HapticProvider } from './providers/haptic';
|
||||||
|
import { HapticsService } from './services/haptics';
|
||||||
|
|
||||||
|
export function configureMobileHapticsModule(framework: Framework) {
|
||||||
|
framework.service(
|
||||||
|
HapticsService,
|
||||||
|
f => new HapticsService(f.getOptional(HapticProvider))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HapticProvider, HapticsService };
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { createIdentifier } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export interface HapticProvider {
|
||||||
|
impact: (options?: { style?: 'HEAVY' | 'LIGHT' | 'MEDIUM' }) => Promise<void>;
|
||||||
|
notification: (options?: {
|
||||||
|
type?: 'SUCCESS' | 'ERROR' | 'WARNING';
|
||||||
|
}) => Promise<void>;
|
||||||
|
vibrate: (options?: { duration?: number }) => Promise<void>;
|
||||||
|
selectionStart: () => Promise<void>;
|
||||||
|
selectionChanged: () => Promise<void>;
|
||||||
|
selectionEnd: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HapticProvider =
|
||||||
|
createIdentifier<HapticProvider>('HapticProvider');
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { HapticProvider } from '../providers/haptic';
|
||||||
|
|
||||||
|
type ExtractArg<T extends keyof HapticProvider> = Parameters<
|
||||||
|
HapticProvider[T]
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
export class HapticsService extends Service {
|
||||||
|
constructor(private readonly provider?: HapticProvider) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
impact(options?: ExtractArg<'impact'>) {
|
||||||
|
this.provider?.impact?.(options)?.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
notification(options?: ExtractArg<'notification'>) {
|
||||||
|
this.provider?.notification?.(options)?.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
vibrate(options?: ExtractArg<'vibrate'>) {
|
||||||
|
this.provider?.vibrate?.(options)?.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionStart() {
|
||||||
|
this.provider?.selectionStart?.().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionChanged() {
|
||||||
|
this.provider?.selectionChanged?.().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionEnd() {
|
||||||
|
this.provider?.selectionEnd?.().catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Framework } from '@toeverything/infra';
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { configureMobileHapticsModule } from './haptics';
|
||||||
import { configureMobileNavigationGestureModule } from './navigation-gesture';
|
import { configureMobileNavigationGestureModule } from './navigation-gesture';
|
||||||
import { configureMobileSearchModule } from './search';
|
import { configureMobileSearchModule } from './search';
|
||||||
import { configureMobileVirtualKeyboardModule } from './virtual-keyboard';
|
import { configureMobileVirtualKeyboardModule } from './virtual-keyboard';
|
||||||
@@ -8,4 +9,5 @@ export function configureMobileModules(framework: Framework) {
|
|||||||
configureMobileSearchModule(framework);
|
configureMobileSearchModule(framework);
|
||||||
configureMobileVirtualKeyboardModule(framework);
|
configureMobileVirtualKeyboardModule(framework);
|
||||||
configureMobileNavigationGestureModule(framework);
|
configureMobileNavigationGestureModule(framework);
|
||||||
|
configureMobileHapticsModule(framework);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SwipeHelper } from '@affine/core/mobile/utils';
|
||||||
import anime from 'animejs';
|
import anime from 'animejs';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { JournalDatePickerContext } from './context';
|
import { JournalDatePickerContext } from './context';
|
||||||
import * as styles from './month.css';
|
import * as styles from './month.css';
|
||||||
import { SwipeHelper } from './swipe-helper';
|
|
||||||
import { getFirstDayOfMonth } from './utils';
|
import { getFirstDayOfMonth } from './utils';
|
||||||
import { WeekRow } from './week';
|
import { WeekRow } from './week';
|
||||||
export interface MonthViewProps {
|
export interface MonthViewProps {
|
||||||
@@ -106,9 +106,9 @@ export const MonthView = ({ viewportHeight }: MonthViewProps) => {
|
|||||||
setCursor(selected);
|
setCursor(selected);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSwipeEnd: ({ deltaX }) => {
|
onSwipeEnd: ({ deltaX, speedX }) => {
|
||||||
setSwiping(false);
|
setSwiping(false);
|
||||||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
|
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD || speedX > 100) {
|
||||||
animateTo(deltaX > 0 ? -1 : 1);
|
animateTo(deltaX > 0 ? -1 : 1);
|
||||||
} else {
|
} else {
|
||||||
animateTo(0);
|
animateTo(0);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useGlobalEvent } from '@affine/core/mobile/hooks/use-global-events';
|
import { useGlobalEvent } from '@affine/core/mobile/hooks/use-global-events';
|
||||||
|
import { SwipeHelper } from '@affine/core/mobile/utils';
|
||||||
import anime from 'animejs';
|
import anime from 'animejs';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { JournalDatePickerContext } from './context';
|
import { JournalDatePickerContext } from './context';
|
||||||
import { MonthView } from './month';
|
import { MonthView } from './month';
|
||||||
import { SwipeHelper } from './swipe-helper';
|
|
||||||
import * as styles from './viewport.css';
|
import * as styles from './viewport.css';
|
||||||
import { WeekHeader, WeekRowSwipe } from './week';
|
import { WeekHeader, WeekRowSwipe } from './week';
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ export const ResizeViewport = ({
|
|||||||
setDragOffset(deltaY);
|
setDragOffset(deltaY);
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
},
|
},
|
||||||
onSwipeEnd: ({ deltaY }) => {
|
onSwipeEnd: ({ deltaY, speedY }) => {
|
||||||
if (Math.abs(deltaY) > 2 * CELL_HEIGHT) {
|
if (Math.abs(deltaY) > 2 * CELL_HEIGHT || speedY > 100) {
|
||||||
handleToggleModeWithAnimation(
|
handleToggleModeWithAnimation(
|
||||||
mode === 'week' ? 'month' : 'week',
|
mode === 'week' ? 'month' : 'week',
|
||||||
deltaY
|
deltaY
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SwipeHelper } from '@affine/core/mobile/utils';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import anime from 'animejs';
|
import anime from 'animejs';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
import { DATE_FORMAT, HORIZONTAL_SWIPE_THRESHOLD } from './constants';
|
import { DATE_FORMAT, HORIZONTAL_SWIPE_THRESHOLD } from './constants';
|
||||||
import { JournalDatePickerContext } from './context';
|
import { JournalDatePickerContext } from './context';
|
||||||
import { DayCell } from './day-cell';
|
import { DayCell } from './day-cell';
|
||||||
import { SwipeHelper } from './swipe-helper';
|
|
||||||
import { getFirstDayOfWeek } from './utils';
|
import { getFirstDayOfWeek } from './utils';
|
||||||
import * as styles from './week.css';
|
import * as styles from './week.css';
|
||||||
|
|
||||||
@@ -115,10 +115,10 @@ export const WeekRowSwipe = ({ start }: WeekRowProps) => {
|
|||||||
setCursor(start);
|
setCursor(start);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSwipeEnd({ deltaX }) {
|
onSwipeEnd({ deltaX, speedX }) {
|
||||||
setSwiping(false);
|
setSwiping(false);
|
||||||
|
|
||||||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
|
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD || speedX > 100) {
|
||||||
animateTo(deltaX > 0 ? -1 : 1);
|
animateTo(deltaX > 0 ? -1 : 1);
|
||||||
} else {
|
} else {
|
||||||
animateTo(0);
|
animateTo(0);
|
||||||
|
|||||||
1
packages/frontend/core/src/mobile/utils/index.ts
Normal file
1
packages/frontend/core/src/mobile/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './swipe-helper';
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
type Direction = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
export interface SwipeInfo {
|
export interface SwipeInfo {
|
||||||
e: TouchEvent;
|
e: TouchEvent;
|
||||||
startX: number;
|
startX: number;
|
||||||
@@ -7,25 +9,60 @@ export interface SwipeInfo {
|
|||||||
deltaX: number;
|
deltaX: number;
|
||||||
deltaY: number;
|
deltaY: number;
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
|
/**
|
||||||
|
* Instant horizontal speed in `px/s`
|
||||||
|
*/
|
||||||
|
speedX: number;
|
||||||
|
/**
|
||||||
|
* Average horizontal speed in `px/s`
|
||||||
|
*/
|
||||||
|
averageSpeedX: number;
|
||||||
|
/**
|
||||||
|
* Instant vertical speed in `px/s`
|
||||||
|
*/
|
||||||
|
speedY: number;
|
||||||
|
/**
|
||||||
|
* Average vertical speed in `px/s`
|
||||||
|
*/
|
||||||
|
averageSpeedY: number;
|
||||||
|
/**
|
||||||
|
* The first detected direction
|
||||||
|
*/
|
||||||
|
initialDirection: Direction | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SwipeHelperOptions {
|
export interface SwipeHelperOptions {
|
||||||
scope?: 'global' | 'inside';
|
scope?: 'global' | 'inside';
|
||||||
|
/**
|
||||||
|
* When swipe started, the direction will be detected and not change until swipe ended.
|
||||||
|
* If the direction is specified, and not match the detected one, the scroll won't be prevented.
|
||||||
|
*/
|
||||||
|
direction?: Direction;
|
||||||
|
/**
|
||||||
|
* @description The distance in px that determine which direction the swipe gesture is
|
||||||
|
* @default 10
|
||||||
|
*/
|
||||||
|
directionThreshold?: number;
|
||||||
preventScroll?: boolean;
|
preventScroll?: boolean;
|
||||||
onTap?: () => void;
|
onTap?: () => void;
|
||||||
onSwipeStart?: () => void;
|
onSwipeStart?: () => void;
|
||||||
onSwipe?: (info: SwipeInfo) => void;
|
onSwipe?: (info: SwipeInfo) => void;
|
||||||
onSwipeEnd?: (info: SwipeInfo) => void;
|
onSwipeEnd?: (info: SwipeInfo) => void;
|
||||||
}
|
}
|
||||||
|
const defaultOptions = Object.freeze({
|
||||||
|
scope: 'global',
|
||||||
|
directionThreshold: 10,
|
||||||
|
} as const);
|
||||||
|
|
||||||
export class SwipeHelper {
|
export class SwipeHelper {
|
||||||
private _trigger: HTMLElement | null = null;
|
private _trigger: HTMLElement | null = null;
|
||||||
private _options: SwipeHelperOptions = {
|
private _options: SwipeHelperOptions = defaultOptions;
|
||||||
scope: 'global',
|
private _direction: Direction | null = null;
|
||||||
};
|
|
||||||
private _startPos: { x: number; y: number } = { x: 0, y: 0 };
|
private _startPos: { x: number; y: number } = { x: 0, y: 0 };
|
||||||
private _isFirst: boolean = true;
|
private _isFirst: boolean = true;
|
||||||
private _lastInfo: SwipeInfo | null = null;
|
private _lastInfo: SwipeInfo | null = null;
|
||||||
|
private _startTime: number = 0;
|
||||||
|
private _lastTime: number = 0;
|
||||||
|
|
||||||
get scopeElement() {
|
get scopeElement() {
|
||||||
return this._options.scope === 'inside'
|
return this._options.scope === 'inside'
|
||||||
@@ -69,6 +106,8 @@ export class SwipeHelper {
|
|||||||
x: touch.clientX,
|
x: touch.clientX,
|
||||||
y: touch.clientY,
|
y: touch.clientY,
|
||||||
};
|
};
|
||||||
|
this._lastTime = Date.now();
|
||||||
|
this._startTime = this._lastTime;
|
||||||
this._options.onSwipeStart?.();
|
this._options.onSwipeStart?.();
|
||||||
const moveHandler = this._handleTouchMove.bind(this);
|
const moveHandler = this._handleTouchMove.bind(this);
|
||||||
this.scopeElement.addEventListener('touchmove', moveHandler, {
|
this.scopeElement.addEventListener('touchmove', moveHandler, {
|
||||||
@@ -87,10 +126,18 @@ export class SwipeHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleTouchMove(e: TouchEvent) {
|
private _handleTouchMove(e: TouchEvent) {
|
||||||
if (this._options.preventScroll) {
|
const info = this._getInfo(e);
|
||||||
|
if (
|
||||||
|
this._options.preventScroll &&
|
||||||
|
// direction is not detected
|
||||||
|
(!this._direction ||
|
||||||
|
// direction is not specified
|
||||||
|
!this._options.direction ||
|
||||||
|
// direction is same as specified
|
||||||
|
this._direction === this._options.direction)
|
||||||
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
const info = this._getInfo(e);
|
|
||||||
this._lastInfo = info;
|
this._lastInfo = info;
|
||||||
this._isFirst = false;
|
this._isFirst = false;
|
||||||
this._options.onSwipe?.(info);
|
this._options.onSwipe?.(info);
|
||||||
@@ -110,9 +157,30 @@ export class SwipeHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getInfo(e: TouchEvent): SwipeInfo {
|
private _getInfo(e: TouchEvent): SwipeInfo {
|
||||||
|
const lastTime = this._lastTime;
|
||||||
|
this._lastTime = Date.now();
|
||||||
|
const deltaTime = this._lastTime - lastTime;
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const deltaX = touch.clientX - this._startPos.x;
|
const deltaX = touch.clientX - this._startPos.x;
|
||||||
const deltaY = touch.clientY - this._startPos.y;
|
const deltaY = touch.clientY - this._startPos.y;
|
||||||
|
const speedX =
|
||||||
|
(Math.abs(deltaX - (this._lastInfo?.deltaX ?? 0)) / deltaTime) * 1000;
|
||||||
|
const averageSpeedX =
|
||||||
|
(Math.abs(deltaX) / (this._lastTime - this._startTime)) * 1000;
|
||||||
|
const speedY =
|
||||||
|
(Math.abs(deltaY - (this._lastInfo?.deltaY ?? 0)) / deltaTime) * 1000;
|
||||||
|
const averageSpeedY =
|
||||||
|
(Math.abs(deltaY) / (this._lastTime - this._startTime)) * 1000;
|
||||||
|
|
||||||
|
// detect direction
|
||||||
|
const threshold =
|
||||||
|
this._options.directionThreshold ?? defaultOptions.directionThreshold;
|
||||||
|
const maxDelta = Math.max(Math.abs(deltaX), Math.abs(deltaY));
|
||||||
|
if (!this._direction && maxDelta > threshold) {
|
||||||
|
this._direction =
|
||||||
|
Math.abs(deltaX) > Math.abs(deltaY) ? 'horizontal' : 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
e,
|
e,
|
||||||
startX: this._startPos.x,
|
startX: this._startPos.x,
|
||||||
@@ -122,11 +190,19 @@ export class SwipeHelper {
|
|||||||
deltaX,
|
deltaX,
|
||||||
deltaY,
|
deltaY,
|
||||||
isFirst: this._isFirst,
|
isFirst: this._isFirst,
|
||||||
|
speedX,
|
||||||
|
averageSpeedX,
|
||||||
|
speedY,
|
||||||
|
averageSpeedY,
|
||||||
|
initialDirection: this._direction,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearDrag() {
|
private _clearDrag() {
|
||||||
this._lastInfo = null;
|
this._lastInfo = null;
|
||||||
|
this._lastTime = 0;
|
||||||
|
this._startTime = 0;
|
||||||
|
this._direction = null;
|
||||||
this._dragMoveCleanup();
|
this._dragMoveCleanup();
|
||||||
this._dragEndCleanup();
|
this._dragEndCleanup();
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ar": 75,
|
"ar": 74,
|
||||||
"ca": 5,
|
"ca": 5,
|
||||||
"da": 6,
|
"da": 6,
|
||||||
"de": 28,
|
"de": 28,
|
||||||
@@ -13,12 +13,12 @@
|
|||||||
"it-IT": 1,
|
"it-IT": 1,
|
||||||
"it": 1,
|
"it": 1,
|
||||||
"ja": 99,
|
"ja": 99,
|
||||||
"ko": 79,
|
"ko": 78,
|
||||||
"pl": 0,
|
"pl": 0,
|
||||||
"pt-BR": 85,
|
"pt-BR": 85,
|
||||||
"ru": 73,
|
"ru": 73,
|
||||||
"sv-SE": 4,
|
"sv-SE": 4,
|
||||||
"ur": 3,
|
"ur": 3,
|
||||||
"zh-Hans": 100,
|
"zh-Hans": 99,
|
||||||
"zh-Hant": 99
|
"zh-Hant": 99
|
||||||
}
|
}
|
||||||
16
yarn.lock
16
yarn.lock
@@ -343,7 +343,7 @@ __metadata:
|
|||||||
"@storybook/react": "npm:^8.2.9"
|
"@storybook/react": "npm:^8.2.9"
|
||||||
"@storybook/react-vite": "npm:^8.2.9"
|
"@storybook/react-vite": "npm:^8.2.9"
|
||||||
"@testing-library/react": "npm:^16.0.0"
|
"@testing-library/react": "npm:^16.0.0"
|
||||||
"@toeverything/theme": "npm:^1.0.18"
|
"@toeverything/theme": "npm:^1.0.21"
|
||||||
"@types/react": "npm:^18.2.75"
|
"@types/react": "npm:^18.2.75"
|
||||||
"@types/react-dom": "npm:^18.2.24"
|
"@types/react-dom": "npm:^18.2.24"
|
||||||
"@vanilla-extract/css": "npm:^1.14.2"
|
"@vanilla-extract/css": "npm:^1.14.2"
|
||||||
@@ -422,7 +422,7 @@ __metadata:
|
|||||||
"@sentry/react": "npm:^8.0.0"
|
"@sentry/react": "npm:^8.0.0"
|
||||||
"@testing-library/react": "npm:^16.0.0"
|
"@testing-library/react": "npm:^16.0.0"
|
||||||
"@toeverything/pdf-viewer": "npm:^0.1.1"
|
"@toeverything/pdf-viewer": "npm:^0.1.1"
|
||||||
"@toeverything/theme": "npm:^1.0.18"
|
"@toeverything/theme": "npm:^1.0.21"
|
||||||
"@types/animejs": "npm:^3.1.12"
|
"@types/animejs": "npm:^3.1.12"
|
||||||
"@types/bytes": "npm:^3.1.4"
|
"@types/bytes": "npm:^3.1.4"
|
||||||
"@types/image-blob-reduce": "npm:^4.1.4"
|
"@types/image-blob-reduce": "npm:^4.1.4"
|
||||||
@@ -618,6 +618,7 @@ __metadata:
|
|||||||
"@capacitor/browser": "npm:^6.0.3"
|
"@capacitor/browser": "npm:^6.0.3"
|
||||||
"@capacitor/cli": "npm:^6.1.2"
|
"@capacitor/cli": "npm:^6.1.2"
|
||||||
"@capacitor/core": "npm:^6.1.2"
|
"@capacitor/core": "npm:^6.1.2"
|
||||||
|
"@capacitor/haptics": "npm:^6.0.2"
|
||||||
"@capacitor/ios": "npm:^6.1.2"
|
"@capacitor/ios": "npm:^6.1.2"
|
||||||
"@capacitor/keyboard": "npm:^6.0.2"
|
"@capacitor/keyboard": "npm:^6.0.2"
|
||||||
"@sentry/react": "npm:^8.0.0"
|
"@sentry/react": "npm:^8.0.0"
|
||||||
@@ -3006,6 +3007,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@capacitor/haptics@npm:^6.0.2":
|
||||||
|
version: 6.0.2
|
||||||
|
resolution: "@capacitor/haptics@npm:6.0.2"
|
||||||
|
peerDependencies:
|
||||||
|
"@capacitor/core": ^6.0.0
|
||||||
|
checksum: 10/986003d8327d914762f5c0a4f4b6e494654fa0c27344c2a1764f06d08895bce035d8105c0f2a571b8b8bd3bd2f72b08bdc2ad4e1167aa186632a7a16bb6118a2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@capacitor/ios@npm:^6.1.2":
|
"@capacitor/ios@npm:^6.1.2":
|
||||||
version: 6.2.0
|
version: 6.2.0
|
||||||
resolution: "@capacitor/ios@npm:6.2.0"
|
resolution: "@capacitor/ios@npm:6.2.0"
|
||||||
@@ -12998,7 +13008,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@toeverything/theme@npm:^1.0.18, @toeverything/theme@npm:^1.0.19":
|
"@toeverything/theme@npm:^1.0.19, @toeverything/theme@npm:^1.0.21":
|
||||||
version: 1.0.21
|
version: 1.0.21
|
||||||
resolution: "@toeverything/theme@npm:1.0.21"
|
resolution: "@toeverything/theme@npm:1.0.21"
|
||||||
checksum: 10/31b741d940b77716825abf7b1a4e64372add4ee4b61439fa59920403c5c615ef72f566857900663d2e5250e09a2e7eae031b0465b98d198b83fc754bac4a8c41
|
checksum: 10/31b741d940b77716825abf7b1a4e64372add4ee4b61439fa59920403c5c615ef72f566857900663d2e5250e09a2e7eae031b0465b98d198b83fc754bac4a8c41
|
||||||
|
|||||||
Reference in New Issue
Block a user