mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-02 15:44:14 +03:00
feat(core): rewrite page-mode-switch with RadioGroup, bind hotkey with cmdk (#7758)
close AF-1170 - bump `@toeverything/theme` - refactor page-mode-switch - use global `<RadioGroup />` - reuse for doc history - remove `styled` usage - bind hotkey via cmdk - Update `<RadioGroup />` color scheme with latest design system - Update right sidebar header tab style - Update tooltip with shortcut for app nav button
This commit is contained in:
parent
4ac9bd7790
commit
75e02bb088
@ -47,7 +47,7 @@
|
||||
"@radix-ui/react-toolbar": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
||||
"@toeverything/theme": "^1.0.2",
|
||||
"@toeverything/theme": "^1.0.4",
|
||||
"@vanilla-extract/dynamic": "^2.1.0",
|
||||
"bytes": "^3.1.2",
|
||||
"check-password-strength": "^2.0.10",
|
||||
|
@ -1,7 +1,14 @@
|
||||
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { createRef, memo, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
createRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { withUnit } from '../../utils/with-unit';
|
||||
import * as styles from './styles.css';
|
||||
@ -71,6 +78,7 @@ export const RadioGroup = memo(function RadioGroup({
|
||||
animationEasing = 'cubic-bezier(.18,.22,0,1)',
|
||||
activeItemClassName,
|
||||
activeItemStyle,
|
||||
iconMode,
|
||||
onChange,
|
||||
}: RadioProps) {
|
||||
const animationTImerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@ -128,23 +136,25 @@ export const RadioGroup = memo(function RadioGroup({
|
||||
[animationDuration, animationEasing, finalItems]
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(newValue: string) => {
|
||||
const oldValue = value;
|
||||
if (oldValue !== newValue) {
|
||||
onChange(newValue);
|
||||
animate(oldValue, newValue);
|
||||
}
|
||||
},
|
||||
[animate, onChange, value]
|
||||
);
|
||||
// animate on value change
|
||||
// useEffect: in case that value is changed from outside
|
||||
const prevValue = useRef(value);
|
||||
useEffect(() => {
|
||||
const currentValue = value;
|
||||
const previousValue = prevValue.current;
|
||||
if (currentValue !== previousValue) {
|
||||
animate(previousValue, currentValue);
|
||||
prevValue.current = currentValue;
|
||||
}
|
||||
}, [animate, value]);
|
||||
|
||||
return (
|
||||
<RadixRadioGroup.Root
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
onValueChange={onChange}
|
||||
className={styles.radioButtonGroup}
|
||||
style={finalStyle}
|
||||
data-icon-mode={iconMode}
|
||||
>
|
||||
{finalItems.map(({ customRender, ...item }, index) => {
|
||||
const testId = item.testId ? { 'data-testid': item.testId } : {};
|
||||
@ -173,9 +183,7 @@ export const RadioGroup = memo(function RadioGroup({
|
||||
ref={item.indicatorRef}
|
||||
/>
|
||||
<span className={styles.radioButtonContent}>
|
||||
{customRender
|
||||
? customRender(item, index)
|
||||
: (item.label ?? item.value)}
|
||||
{customRender?.(item, index) ?? item.label ?? item.value}
|
||||
</span>
|
||||
</RadixRadioGroup.Item>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const outerPadding = createVar('radio-outer-padding');
|
||||
@ -16,16 +17,22 @@ export const radioButton = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
color: cssVarV2('switch/fontColor/tertiary'),
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
fontWeight: 600,
|
||||
selectors: {
|
||||
'&[data-state="checked"]': {
|
||||
color: cssVar('textPrimaryColor'),
|
||||
color: cssVarV2('switch/fontColor/primary'),
|
||||
},
|
||||
'&[data-state="unchecked"]:hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
background: cssVarV2('switch/buttonBackground/hover'),
|
||||
},
|
||||
'[data-icon-mode=true] &': {
|
||||
color: cssVarV2('switch/iconColor/default'),
|
||||
},
|
||||
'[data-icon-mode=true] &[data-state="checked"]': {
|
||||
color: cssVarV2('switch/iconColor/active'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -37,7 +44,7 @@ globalStyle(`${radioButtonContent} > svg`, { display: 'block' });
|
||||
export const radioButtonGroup = style({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
background: cssVar('hoverColorFilled'),
|
||||
background: cssVarV2('switch/switchBackground/background'),
|
||||
|
||||
borderRadius: outerRadius,
|
||||
padding: outerPadding,
|
||||
@ -53,8 +60,8 @@ export const indicator = style({
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
background: cssVar('white'),
|
||||
filter: 'drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1))',
|
||||
background: cssVarV2('switch/buttonBackground/active'),
|
||||
boxShadow: cssVar('buttonShadow'),
|
||||
opacity: 0,
|
||||
transformOrigin: 'left',
|
||||
selectors: {
|
||||
|
@ -47,6 +47,10 @@ export interface RadioProps extends RadioGroupItemProps {
|
||||
activeItemClassName?: string;
|
||||
/** Customize active item's style */
|
||||
activeItemStyle?: CSSProperties;
|
||||
/**
|
||||
* This prop is used to use a different color scheme
|
||||
*/
|
||||
iconMode?: boolean;
|
||||
}
|
||||
|
||||
export interface RadioItem {
|
||||
|
@ -48,7 +48,7 @@
|
||||
"@sentry/integrations": "^7.109.0",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@sgtpooki/file-type": "^1.0.1",
|
||||
"@toeverything/theme": "^1.0.2",
|
||||
"@toeverything/theme": "^1.0.4",
|
||||
"@vanilla-extract/dynamic": "^2.1.0",
|
||||
"animejs": "^3.2.2",
|
||||
"async-call-rpc": "^6.4.2",
|
||||
|
@ -34,11 +34,7 @@ import { encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
||||
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
||||
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
||||
import {
|
||||
EdgelessSwitchItem,
|
||||
PageSwitchItem,
|
||||
} from '../../blocksuite/block-suite-mode-switch/switch-items';
|
||||
import { PureEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch';
|
||||
import { AffineErrorBoundary } from '../affine-error-boundary';
|
||||
import {
|
||||
historyListGroupByDay,
|
||||
@ -108,15 +104,11 @@ const HistoryEditorPreview = ({
|
||||
mode,
|
||||
title,
|
||||
}: HistoryEditorPreviewProps) => {
|
||||
const { onSwitchToPageMode, onSwitchToEdgelessMode } = useMemo(
|
||||
() => ({
|
||||
onSwitchToPageMode: () => {
|
||||
onModeChange('page');
|
||||
},
|
||||
onSwitchToEdgelessMode: () => {
|
||||
onModeChange('edgeless');
|
||||
},
|
||||
}),
|
||||
const onModeChangeWithTrack = useCallback(
|
||||
(mode: DocMode) => {
|
||||
track.$.docHistory.$.switchPageMode({ mode });
|
||||
onModeChange(mode);
|
||||
},
|
||||
[onModeChange]
|
||||
);
|
||||
|
||||
@ -124,22 +116,7 @@ const HistoryEditorPreview = ({
|
||||
return (
|
||||
<div className={styles.previewContent}>
|
||||
<div className={styles.previewHeader}>
|
||||
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
|
||||
<PageSwitchItem
|
||||
data-testid="switch-page-mode-button"
|
||||
active={mode === 'page'}
|
||||
data-event-props="$.docHistory.$.switchPageMode"
|
||||
data-event-args-type="page"
|
||||
onClick={onSwitchToPageMode}
|
||||
/>
|
||||
<EdgelessSwitchItem
|
||||
data-testid="switch-edgeless-mode-button"
|
||||
active={mode === 'edgeless'}
|
||||
data-event-props="$.docHistory.$.switchPageMode"
|
||||
data-event-args-type="edgeless"
|
||||
onClick={onSwitchToEdgelessMode}
|
||||
/>
|
||||
</StyledEditorModeSwitch>
|
||||
<PureEditorModeSwitch mode={mode} setMode={onModeChangeWithTrack} />
|
||||
<div className={styles.previewHeaderTitle}>{title}</div>
|
||||
<div className={styles.previewHeaderTimestamp}>
|
||||
{ts
|
||||
@ -170,14 +147,7 @@ const HistoryEditorPreview = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
mode,
|
||||
onSwitchToEdgelessMode,
|
||||
onSwitchToPageMode,
|
||||
snapshotPage,
|
||||
title,
|
||||
ts,
|
||||
]);
|
||||
}, [mode, onModeChangeWithTrack, snapshotPage, title, ts]);
|
||||
|
||||
return (
|
||||
<div className={styles.previewWrapper}>
|
||||
|
@ -1,87 +1,69 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { RadioGroup, type RadioItem, toast, Tooltip } from '@affine/component';
|
||||
import { registerAffineCommand } from '@affine/core/commands';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
type DocMode,
|
||||
DocService,
|
||||
DocsService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import type { DocCollection } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { StyledEditorModeSwitch } from './style';
|
||||
import { switchItem } from './style.css';
|
||||
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
|
||||
|
||||
export type EditorModeSwitchProps = {
|
||||
// todo(himself65): combine these two properties
|
||||
docCollection: DocCollection;
|
||||
export interface EditorModeSwitchProps {
|
||||
pageId: string;
|
||||
style?: CSSProperties;
|
||||
isPublic?: boolean;
|
||||
publicMode?: DocMode;
|
||||
}
|
||||
|
||||
const EdgelessRadioItem: RadioItem = {
|
||||
value: 'edgeless',
|
||||
label: <EdgelessSwitchItem />,
|
||||
testId: 'switch-edgeless-mode-button',
|
||||
className: switchItem,
|
||||
};
|
||||
const PageRadioItem: RadioItem = {
|
||||
value: 'page',
|
||||
label: <PageSwitchItem />,
|
||||
testId: 'switch-page-mode-button',
|
||||
className: switchItem,
|
||||
};
|
||||
|
||||
export const EditorModeSwitch = ({
|
||||
style,
|
||||
docCollection,
|
||||
pageId,
|
||||
isPublic,
|
||||
publicMode,
|
||||
}: EditorModeSwitchProps) => {
|
||||
const t = useI18n();
|
||||
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const trash = pageMeta?.trash ?? false;
|
||||
const doc = useService(DocService).doc;
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(pageId));
|
||||
const trash = useLiveData(doc?.trash$);
|
||||
const currentMode = useLiveData(doc?.mode$);
|
||||
|
||||
const currentMode = useLiveData(doc.mode$);
|
||||
|
||||
useEffect(() => {
|
||||
if (trash || isPublic) {
|
||||
return;
|
||||
}
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if (e.code === 'KeyS' && e.altKey) {
|
||||
e.preventDefault();
|
||||
doc.toggleMode();
|
||||
toast(
|
||||
currentMode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
: t['com.affine.toastMessage.pageMode']()
|
||||
);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [currentMode, isPublic, doc, pageId, t, trash]);
|
||||
|
||||
const onSwitchToPageMode = useCallback(() => {
|
||||
track.$.header.actions.switchPageMode({
|
||||
mode: 'page',
|
||||
});
|
||||
if (currentMode === 'page' || isPublic) {
|
||||
return;
|
||||
}
|
||||
doc.setMode('page');
|
||||
const togglePage = useCallback(() => {
|
||||
if (currentMode === 'page' || isPublic || trash) return;
|
||||
doc?.setMode('page');
|
||||
toast(t['com.affine.toastMessage.pageMode']());
|
||||
}, [currentMode, isPublic, doc, t]);
|
||||
track.$.header.actions.switchPageMode({ mode: 'page' });
|
||||
}, [currentMode, doc, isPublic, t, trash]);
|
||||
|
||||
const onSwitchToEdgelessMode = useCallback(() => {
|
||||
track.$.header.actions.switchPageMode({
|
||||
mode: 'edgeless',
|
||||
});
|
||||
if (currentMode === 'edgeless' || isPublic) {
|
||||
return;
|
||||
}
|
||||
doc.setMode('edgeless');
|
||||
const toggleEdgeless = useCallback(() => {
|
||||
if (currentMode === 'edgeless' || isPublic || trash) return;
|
||||
doc?.setMode('edgeless');
|
||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
||||
}, [currentMode, isPublic, doc, t]);
|
||||
track.$.header.actions.switchPageMode({ mode: 'edgeless' });
|
||||
}, [currentMode, doc, isPublic, t, trash]);
|
||||
|
||||
const onModeChange = useCallback(
|
||||
(mode: DocMode) => {
|
||||
mode === 'page' ? togglePage() : toggleEdgeless();
|
||||
},
|
||||
[toggleEdgeless, togglePage]
|
||||
);
|
||||
|
||||
const shouldHide = useCallback(
|
||||
(mode: DocMode) =>
|
||||
@ -89,40 +71,73 @@ export const EditorModeSwitch = ({
|
||||
[currentMode, isPublic, publicMode, trash]
|
||||
);
|
||||
|
||||
const shouldActive = useCallback(
|
||||
(mode: DocMode) => (isPublic ? false : currentMode === mode),
|
||||
[currentMode, isPublic]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (trash || isPublic || currentMode === undefined) return;
|
||||
return registerAffineCommand({
|
||||
id: 'affine:doc-mode-switch',
|
||||
category: 'editor:page',
|
||||
label:
|
||||
currentMode === 'page'
|
||||
? t['com.affine.cmdk.switch-to-edgeless']()
|
||||
: t['com.affine.cmdk.switch-to-page'](),
|
||||
icon: currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />,
|
||||
keyBinding: {
|
||||
binding: 'Alt+KeyS',
|
||||
capture: true,
|
||||
},
|
||||
run: () => onModeChange(currentMode === 'edgeless' ? 'page' : 'edgeless'),
|
||||
});
|
||||
}, [currentMode, isPublic, onModeChange, t, trash]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={t['Switch']()}
|
||||
shortcut={['$alt', 'S']}
|
||||
side="bottom"
|
||||
options={{
|
||||
hidden: isPublic || trash,
|
||||
}}
|
||||
options={{ hidden: isPublic || trash }}
|
||||
>
|
||||
<StyledEditorModeSwitch
|
||||
style={style}
|
||||
switchLeft={currentMode === 'page'}
|
||||
showAlone={trash || isPublic}
|
||||
>
|
||||
<PageSwitchItem
|
||||
data-testid="switch-page-mode-button"
|
||||
active={shouldActive('page')}
|
||||
hide={shouldHide('page')}
|
||||
trash={trash}
|
||||
onClick={onSwitchToPageMode}
|
||||
<div>
|
||||
<PureEditorModeSwitch
|
||||
mode={currentMode}
|
||||
setMode={onModeChange}
|
||||
hidePage={shouldHide('page')}
|
||||
hideEdgeless={shouldHide('edgeless')}
|
||||
/>
|
||||
<EdgelessSwitchItem
|
||||
data-testid="switch-edgeless-mode-button"
|
||||
active={shouldActive('edgeless')}
|
||||
hide={shouldHide('edgeless')}
|
||||
trash={trash}
|
||||
onClick={onSwitchToEdgelessMode}
|
||||
/>
|
||||
</StyledEditorModeSwitch>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PureEditorModeSwitchProps {
|
||||
mode?: DocMode;
|
||||
setMode: (mode: DocMode) => void;
|
||||
hidePage?: boolean;
|
||||
hideEdgeless?: boolean;
|
||||
}
|
||||
|
||||
export const PureEditorModeSwitch = ({
|
||||
mode,
|
||||
setMode,
|
||||
hidePage,
|
||||
hideEdgeless,
|
||||
}: PureEditorModeSwitchProps) => {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
...(hidePage ? [] : [PageRadioItem]),
|
||||
...(hideEdgeless ? [] : [EdgelessRadioItem]),
|
||||
],
|
||||
[hideEdgeless, hidePage]
|
||||
);
|
||||
return (
|
||||
<RadioGroup
|
||||
iconMode
|
||||
itemHeight={24}
|
||||
borderRadius={8}
|
||||
padding={4}
|
||||
gap={8}
|
||||
value={mode}
|
||||
items={items}
|
||||
onChange={setMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const switchItem = style({
|
||||
width: 24,
|
||||
height: 24,
|
||||
});
|
||||
// a workaround to override lottie svg stroke with default radio button color schemes
|
||||
globalStyle(`${switchItem} svg path`, {
|
||||
stroke: 'currentColor',
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
|
||||
// TODO(@CatsJuice): refactor this component
|
||||
export const StyledEditorModeSwitch = styled('div')<{
|
||||
switchLeft: boolean;
|
||||
showAlone?: boolean;
|
||||
}>(({ switchLeft, showAlone }) => {
|
||||
return {
|
||||
maxWidth: showAlone ? '40px' : '70px',
|
||||
gap: '8px',
|
||||
height: '32px',
|
||||
background: showAlone
|
||||
? 'transparent'
|
||||
: 'var(--affine-background-secondary-color)',
|
||||
borderRadius: '8px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
padding: '4px 4px',
|
||||
position: 'relative',
|
||||
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: showAlone ? 'none' : 'block',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
borderRadius: '4px',
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
transform: `translateX(${switchLeft ? '0' : '32px'})`,
|
||||
transition: 'all .15s',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSwitchItem = styled('button')<{
|
||||
active?: boolean;
|
||||
hide?: boolean;
|
||||
trash?: boolean;
|
||||
}>(({ active = false, hide = false, trash = false }) => {
|
||||
return {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '8px',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
boxShadow: active ? 'var(--affine-shadow-1)' : 'none',
|
||||
color: active
|
||||
? trash
|
||||
? 'var(--affine-error-color)'
|
||||
: 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
display: hide ? 'none' : 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
fontSize: '20px',
|
||||
path: {
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
};
|
||||
});
|
@ -5,35 +5,23 @@ import { cloneElement, useState } from 'react';
|
||||
|
||||
import edgelessHover from './animation-data/edgeless-hover.json';
|
||||
import pageHover from './animation-data/page-hover.json';
|
||||
import { StyledSwitchItem } from './style';
|
||||
|
||||
type HoverAnimateControllerProps = {
|
||||
active?: boolean;
|
||||
hide?: boolean;
|
||||
trash?: boolean;
|
||||
children: React.ReactElement;
|
||||
} & HTMLAttributes<HTMLButtonElement>;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const HoverAnimateController = ({
|
||||
active,
|
||||
hide,
|
||||
trash,
|
||||
children,
|
||||
...props
|
||||
}: HoverAnimateControllerProps) => {
|
||||
const [startAnimate, setStartAnimate] = useState(false);
|
||||
return (
|
||||
<StyledSwitchItem
|
||||
hide={hide}
|
||||
active={active}
|
||||
data-active={active}
|
||||
trash={trash}
|
||||
onMouseEnter={() => {
|
||||
setStartAnimate(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setStartAnimate(false);
|
||||
}}
|
||||
<div
|
||||
onMouseEnter={() => setStartAnimate(true)}
|
||||
onMouseLeave={() => setStartAnimate(false)}
|
||||
{...props}
|
||||
>
|
||||
{cloneElement(children, {
|
||||
@ -42,7 +30,7 @@ const HoverAnimateController = ({
|
||||
width: 20,
|
||||
height: 20,
|
||||
})}
|
||||
</StyledSwitchItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,9 +2,8 @@ import { IconButton } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useGeneralShortcuts } from '../../../hooks/affine/use-shortcuts';
|
||||
import { NavigatorService } from '../services/navigator';
|
||||
import * as styles from './navigation-buttons.css';
|
||||
|
||||
@ -13,22 +12,6 @@ const tooltipSideBottom = { side: 'bottom' as const };
|
||||
export const NavigationButtons = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const shortcuts = useGeneralShortcuts().shortcuts;
|
||||
|
||||
const shortcutsObject = useMemo(() => {
|
||||
const goBack = t['com.affine.keyboardShortcuts.goBack']();
|
||||
const goBackShortcut = shortcuts?.[goBack];
|
||||
|
||||
const goForward = t['com.affine.keyboardShortcuts.goForward']();
|
||||
const goForwardShortcut = shortcuts?.[goForward];
|
||||
return {
|
||||
goBack,
|
||||
goBackShortcut,
|
||||
goForward,
|
||||
goForwardShortcut,
|
||||
};
|
||||
}, [shortcuts, t]);
|
||||
|
||||
const navigator = useService(NavigatorService).navigator;
|
||||
|
||||
const backable = useLiveData(navigator.backable$);
|
||||
@ -65,11 +48,11 @@ export const NavigationButtons = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO(@CatsJuice): tooltip with shortcut
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<IconButton
|
||||
tooltip={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
|
||||
tooltip={t['Go Back']()}
|
||||
tooltipShortcut={['$mod', '[']}
|
||||
tooltipOptions={tooltipSideBottom}
|
||||
className={styles.button}
|
||||
data-testid="app-navigation-button-back"
|
||||
@ -80,7 +63,8 @@ export const NavigationButtons = () => {
|
||||
<ArrowLeftSmallIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
tooltip={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
|
||||
tooltip={t['Go Forward']()}
|
||||
tooltipShortcut={['$mod', ']']}
|
||||
tooltipOptions={tooltipSideBottom}
|
||||
className={styles.button}
|
||||
data-testid="app-navigation-button-forward"
|
||||
|
@ -289,6 +289,12 @@ const CMDKKeyBinding = ({ keyBinding }: { keyBinding: string }) => {
|
||||
if (fragment === '$mod') {
|
||||
return isMacOS ? '⌘' : 'Ctrl';
|
||||
}
|
||||
if (fragment === 'Alt') {
|
||||
return isMacOS ? '⌥' : 'Alt';
|
||||
}
|
||||
if (fragment.startsWith('Key')) {
|
||||
return fragment.slice(3);
|
||||
}
|
||||
if (fragment === 'ArrowUp') {
|
||||
return '↑';
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { RadioGroup } from '@affine/component';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ViewService } from '../../services/view';
|
||||
@ -35,6 +34,7 @@ export const SidebarHeaderSwitcher = () => {
|
||||
|
||||
return tabItems.length ? (
|
||||
<RadioGroup
|
||||
iconMode
|
||||
borderRadius={8}
|
||||
itemHeight={24}
|
||||
padding={4}
|
||||
@ -42,7 +42,6 @@ export const SidebarHeaderSwitcher = () => {
|
||||
items={tabItems}
|
||||
value={activeTab?.id}
|
||||
onChange={handleActiveTabChange}
|
||||
activeItemStyle={{ color: cssVar('primaryColor') }}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
@ -17,12 +17,7 @@ export function ShareHeader({
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<EditorModeSwitch
|
||||
isPublic
|
||||
docCollection={docCollection}
|
||||
pageId={pageId}
|
||||
publicMode={publishMode}
|
||||
/>
|
||||
<EditorModeSwitch isPublic pageId={pageId} publicMode={publishMode} />
|
||||
<BlocksuiteHeaderTitle
|
||||
docCollection={docCollection}
|
||||
pageId={pageId}
|
||||
|
@ -81,10 +81,7 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
<Header className={styles.header} ref={containerRef}>
|
||||
<ViewTitle title={title} />
|
||||
<ViewIcon icon="journal" />
|
||||
<EditorModeSwitch
|
||||
docCollection={workspace.docCollection}
|
||||
pageId={page?.id}
|
||||
/>
|
||||
<EditorModeSwitch pageId={page?.id} />
|
||||
<div className={styles.journalWeekPicker}>
|
||||
<JournalWeekDatePicker
|
||||
docCollection={workspace.docCollection}
|
||||
@ -138,10 +135,7 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
<Header className={styles.header} ref={containerRef}>
|
||||
<ViewTitle title={title} />
|
||||
<ViewIcon icon={currentMode ?? 'page'} />
|
||||
<EditorModeSwitch
|
||||
docCollection={workspace.docCollection}
|
||||
pageId={page?.id}
|
||||
/>
|
||||
<EditorModeSwitch pageId={page?.id} />
|
||||
<BlocksuiteHeaderTitle
|
||||
inputHandleRef={titleInputHandleRef}
|
||||
pageId={page?.id}
|
||||
|
@ -600,6 +600,8 @@
|
||||
"com.affine.cmdk.no-results": "No results found",
|
||||
"com.affine.cmdk.no-results-for": "No results found for",
|
||||
"com.affine.cmdk.placeholder": "Type a command or search anything...",
|
||||
"com.affine.cmdk.switch-to-edgeless": "Switch to $t(com.affine.edgelessMode)",
|
||||
"com.affine.cmdk.switch-to-page": "Switch to $t(com.affine.pageMode)",
|
||||
"com.affine.collection-bar.action.tooltip.delete": "Delete",
|
||||
"com.affine.collection-bar.action.tooltip.edit": "Edit",
|
||||
"com.affine.collection-bar.action.tooltip.pin": "Pin to sidebar",
|
||||
|
@ -1,54 +1,42 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export function locateModeSwitchButton(
|
||||
page: Page,
|
||||
mode: 'page' | 'edgeless',
|
||||
active?: boolean
|
||||
) {
|
||||
// switch is implemented as RadioGroup button,
|
||||
// so we can use aria-checked to determine the active state
|
||||
const checkedSelector = active ? '[aria-checked="true"]' : '';
|
||||
|
||||
return page.locator(
|
||||
`[data-testid="switch-${mode}-mode-button"]${checkedSelector}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function clickEdgelessModeButton(page: Page) {
|
||||
await page.getByTestId('switch-edgeless-mode-button').click({
|
||||
delay: 50,
|
||||
});
|
||||
await expect(
|
||||
page.locator(
|
||||
'[data-testid="switch-edgeless-mode-button"][data-active="true"]'
|
||||
)
|
||||
).toBeVisible();
|
||||
await locateModeSwitchButton(page, 'edgeless').click({ delay: 50 });
|
||||
await ensureInEdgelessMode(page);
|
||||
}
|
||||
|
||||
export async function clickPageModeButton(page: Page) {
|
||||
await page.getByTestId('switch-page-mode-button').click({
|
||||
delay: 50,
|
||||
});
|
||||
await expect(
|
||||
page.locator('[data-testid="switch-page-mode-button"][data-active="true"]')
|
||||
).toBeVisible();
|
||||
await locateModeSwitchButton(page, 'page').click({ delay: 50 });
|
||||
await ensureInPageMode(page);
|
||||
}
|
||||
|
||||
export async function ensureInPageMode(page: Page) {
|
||||
await expect(
|
||||
page.locator('[data-testid="switch-page-mode-button"][data-active="true"]')
|
||||
).toBeVisible();
|
||||
await expect(locateModeSwitchButton(page, 'page', true)).toBeVisible();
|
||||
}
|
||||
|
||||
export async function ensureInEdgelessMode(page: Page) {
|
||||
await expect(
|
||||
page.locator(
|
||||
'[data-testid="switch-edgeless-mode-button"][data-active="true"]'
|
||||
)
|
||||
).toBeVisible();
|
||||
await expect(locateModeSwitchButton(page, 'edgeless', true)).toBeVisible();
|
||||
}
|
||||
|
||||
export async function getPageMode(page: Page): Promise<'page' | 'edgeless'> {
|
||||
if (
|
||||
await page
|
||||
.locator('[data-testid="switch-page-mode-button"][data-active="true"]')
|
||||
.isVisible()
|
||||
) {
|
||||
if (await locateModeSwitchButton(page, 'page', true).isVisible()) {
|
||||
return 'page';
|
||||
}
|
||||
if (
|
||||
await page
|
||||
.locator(
|
||||
'[data-testid="switch-edgeless-mode-button"][data-active="true"]'
|
||||
)
|
||||
.isVisible()
|
||||
) {
|
||||
if (await locateModeSwitchButton(page, 'edgeless', true).isVisible()) {
|
||||
return 'edgeless';
|
||||
}
|
||||
throw new Error('Unknown mode');
|
||||
|
12
yarn.lock
12
yarn.lock
@ -324,7 +324,7 @@ __metadata:
|
||||
"@storybook/test-runner": "npm:^0.19.0"
|
||||
"@storybook/testing-library": "npm:^0.2.2"
|
||||
"@testing-library/react": "npm:^16.0.0"
|
||||
"@toeverything/theme": "npm:^1.0.2"
|
||||
"@toeverything/theme": "npm:^1.0.4"
|
||||
"@types/bytes": "npm:^3.1.4"
|
||||
"@types/react": "npm:^18.2.75"
|
||||
"@types/react-dnd": "npm:^3.0.2"
|
||||
@ -419,7 +419,7 @@ __metadata:
|
||||
"@sgtpooki/file-type": "npm:^1.0.1"
|
||||
"@swc/core": "npm:^1.4.13"
|
||||
"@testing-library/react": "npm:^16.0.0"
|
||||
"@toeverything/theme": "npm:^1.0.2"
|
||||
"@toeverything/theme": "npm:^1.0.4"
|
||||
"@types/animejs": "npm:^3.1.12"
|
||||
"@types/bytes": "npm:^3.1.4"
|
||||
"@types/image-blob-reduce": "npm:^4.1.4"
|
||||
@ -14946,10 +14946,10 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@toeverything/theme@npm:^1.0.2":
|
||||
version: 1.0.3
|
||||
resolution: "@toeverything/theme@npm:1.0.3"
|
||||
checksum: 10/2e1df60e417717aee7525d1ec0432462f95cfaded3da6ad5d7c4b273cd97b77bd3948669f56be06b592e2c6a51cd892902b7b18ba2b71360ccc0a9daba18b315
|
||||
"@toeverything/theme@npm:^1.0.2, @toeverything/theme@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "@toeverything/theme@npm:1.0.4"
|
||||
checksum: 10/88d7cb0c2f77e1632c9a25bb6605c8df22b8b41cd7f76435c0d64beb9f4758a56ed48fb35965588c004a97634b8097ea4ebc0ea44747ce46db0e88d9a0527d26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user